Skip to content

Commit

Permalink
Rewrite filename guessing logic
Browse files Browse the repository at this point in the history
 - Closes #261
  • Loading branch information
marklieberman committed Jul 16, 2018
1 parent a6778c6 commit bfaf5df
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 22 deletions.
27 changes: 26 additions & 1 deletion gulpfile.js
@@ -1,7 +1,8 @@
var gulp = require('gulp'),
jshint = require('gulp-jshint'),
jsonlint = require("gulp-jsonlint");
jsonlint = require("gulp-jsonlint"),
sass = require('gulp-sass'),
karma = require('karma'),
zip = require('gulp-zip');

var sources = {
Expand All @@ -21,6 +22,19 @@ var sources = {
]
};

// Default Karma configuration.
var karmaConfig = {
browsers: ['FirefoxHeadless'],
customLaunchers: {
FirefoxHeadless: {
base: 'Firefox',
flags: [ '-headless' ],
},
},
frameworks: [ 'jasmine' ],
singleRun: true
};

gulp.task('default', [ 'lint', 'jsonlint', 'sass', 'watch' ]);

gulp.task('watch', [ 'sass' ], function () {
Expand All @@ -47,6 +61,17 @@ gulp.task('jsonlint', function () {
.pipe(jsonlint.reporter());
});

gulp.task('test', function (done) {
// background/helpers.js
new karma.Server(Object.assign(karmaConfig, {
files: [
'test/background/webex-mocks.js',
'src/background/helpers.js',
'test/background/helpers.spec.js'
]
}), done).start();
});

gulp.task('dist', [ 'sass' ], function () {
return gulp.src(sources.dist)
.pipe(zip('foxygestures.xpi', {
Expand Down
6 changes: 5 additions & 1 deletion package.json
Expand Up @@ -21,8 +21,12 @@
"gulp-jsonlint": "^1.2.1",
"gulp-sass": "^3.1.0",
"gulp-zip": "^4.0.0",
"jasmine-core": "^3.1.0",
"jshint": "^2.9.4",
"jshint-stylish": "^2.2.1"
"jshint-stylish": "^2.2.1",
"karma": "^2.0.4",
"karma-firefox-launcher": "^1.1.0",
"karma-jasmine": "^1.1.2"
},
"dependencies": {
"bootstrap-sass": "^3.3.7"
Expand Down
58 changes: 38 additions & 20 deletions src/background/helpers.js
Expand Up @@ -26,14 +26,15 @@ modules.helpers = (function (module) {
// Document
'text/plain': '.txt',
'text/html': '.html',
'text/css' : '.css',
'text/javascript' : '.js',
'text/css': '.css',
'text/javascript': '.js',
'text/json': '.json',
// Image
'image/png' : '.png',
'image/jpeg' : '.jpg',
'image/gif' : '.gif',
'image/bmp' : '.bmp',
'image/webp' : '.webp',
'image/png': '.png',
'image/jpeg': '.jpg',
'image/gif': '.gif',
'image/bmp': '.bmp',
'image/webp': '.webp',
// Video
'video/mp4': '.mp4',
'video/ogg': '.ogg',
Expand All @@ -48,6 +49,7 @@ modules.helpers = (function (module) {
'audio/wave': '.wav',
'audio/webm': '.webm',
// Other
'application/json': '.json',
'application/octet-stream': '.bin'
};

Expand Down Expand Up @@ -85,9 +87,14 @@ modules.helpers = (function (module) {
return format;
};

// Remove invalid characters in a Windows path.
module.cleanPath = (input, replace = '') => {
return input.replace(/[\\/:"*?<>|]+/gi, replace);
};

// Attempt to determine the filename from a media URL. If the media source does not contain a file extension but the
// mime type is known, select the extension automatically.
module.suggestFilename = (mediaSource, mediaType) => {
module.suggestFilename = (mediaSource, mediaType = null) => {
// Data URIs do not have a file name so try generate a name like 'data.ext' using mime type.
if (mediaSource.startsWith('data:')) {
// Extract the mime type if present.
Expand All @@ -98,19 +105,30 @@ modules.helpers = (function (module) {
return (mime === '') ? 'data.txt' : 'data' + (mimeToExtensionMap[mime] || '.bin');
}

// Extract the filename from the URL.
let match = /\/([^\/?#]+)($|\?|#)/i.exec(decodeURI(mediaSource));
if (match && match[1]) {
// Extract the extension from the filename.
match = /([^.]+)(\.[a-z0-9]+)?/i.exec(match[1]);
if (match && match[2]) {
// Filename seems to have an extension
return match[1] + match[2];
} else {
// Try to guess the extension from the type.
return match[1] + (mimeToExtensionMap[mediaType] || '');
try {
// Extract the filename from the URL.
// Take everything from the final / to the query, fragment, or end of URL.
mediaSource = decodeURI(mediaSource);
let match = /\/([^\/?#]+)($|\?|#)/i.exec(mediaSource);
if (mediaSource && match && match[1]) {
let basename = match[1];
basename = decodeURIComponent(basename);
basename = module.cleanPath(basename, ' ');

// Split the basename into file and extension.
let lastDot = basename.lastIndexOf('.');
if (!!~lastDot) {
let filename = basename.substring(0, lastDot);
let extension = basename.substring(lastDot);

// Trim any trailing text from the extension.
return filename + /\.\w+/.exec(extension);
} else {
// Try to guess the extension from the type.
return basename + (mimeToExtensionMap[mediaType] || '');
}
}
}
} catch (error) {}

// Couldn't determine the filename; let the browser guess.
return null;
Expand Down
75 changes: 75 additions & 0 deletions test/background/helpers.spec.js
@@ -0,0 +1,75 @@
'use strict';

describe("background/helper.js", function() {

var helpers;

beforeEach(function () {
helpers = modules.helpers;
});

it("should suggest filenames for normal URLs", function() {
// Return null so the browser can guess the filename.
expect(helpers.suggestFilename('https://www.example.com/'))
.toBe(null);

// Simple URLs with normal features.
expect(helpers.suggestFilename('https://www.example.com/example.html'))
.toBe('example.html');
expect(helpers.suggestFilename('https://www.example.com/example.html?q=search'))
.toBe('example.html');
expect(helpers.suggestFilename('https://www.example.com/example.html?q=search#anchor'))
.toBe('example.html');
expect(helpers.suggestFilename('https://www.example.com/example.html#anchor'))
.toBe('example.html');
expect(helpers.suggestFilename('https://www.example.com/example.jpg'))
.toBe('example.jpg');
expect(helpers.suggestFilename('https://www.example.com/subdir/example.jpg'))
.toBe('example.jpg');
expect(helpers.suggestFilename('https://www.example.com/subdir/example'))
.toBe('example');
expect(helpers.suggestFilename('https://www.example.com/subdir/video.mp4'))
.toBe('video.mp4');

// Simple URLs with characters that are not valid for paths.
expect(helpers.suggestFilename('https://www.example.com/bad:file.txt'))
.toBe('bad file.txt');
expect(helpers.suggestFilename('https://www.example.com/bad%3Ffile.txt'))
.toBe('bad file.txt');

// Examples from #251 - trailing text after extension.
expect(helpers.suggestFilename('https://pbs.twimg.com/media/Deh3bIAXUAAfyxP.jpg:large'))
.toBe('Deh3bIAXUAAfyxP.jpg');

// Examples from #261 - repeated dots in filename.
expect(helpers.suggestFilename('https://www.example.com/string01.string02_string03.jpg'))
.toBe('string01.string02_string03.jpg');
expect(helpers.suggestFilename('https://www.example.com/string01.string02/string03.string04_string05.jpg'))
.toBe('string03.string04_string05.jpg');

// Video with no extension but mime-type is known.
expect(helpers.suggestFilename('https://www.example.com/sample_video', 'video/webm'))
.toBe('sample_video.webm');
});

it("should suggest filenames for data: URLs", function() {
// No mime-type should default to text.
expect(helpers.suggestFilename('data:,Hello%2C%20World!')).toBe('data.txt');

// Known mime-types should get valid extensions.
expect(helpers.suggestFilename('data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D')).toBe('data.txt');
expect(helpers.suggestFilename('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E')).toBe('data.html');
expect(helpers.suggestFilename('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7')).toBe('data.gif');
expect(helpers.suggestFilename('data:application/octet-stream;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7')).toBe('data.bin');

// Unknown mime type defaults to .bin
expect(helpers.suggestFilename('data:unknown/mime;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7')).toBe('data.bin');
});

it("should suggest filenames and handle invalid URLs", function() {
// URL contains an invalid escape sequence.
expect(helpers.suggestFilename('https://www.example.com/bad-sequence%E0%A4%A.jpg'))
.toBe(null);
});

});
5 changes: 5 additions & 0 deletions test/background/webex-mocks.js
@@ -0,0 +1,5 @@
var browser = {
i18n: jasmine.createSpyObj([
'getMessage'
])
};

0 comments on commit bfaf5df

Please sign in to comment.