Content disposition filename

File name is extracted from headers.
This commit is contained in:
Juan Salvador Perez Garcia 2018-01-13 09:01:50 +01:00 committed by Jonas Jenwald
parent 96c573ad38
commit eb1f6f4c24
8 changed files with 139 additions and 21 deletions

View File

@ -33,7 +33,7 @@
"uglify-es": "^3.1.2", "uglify-es": "^3.1.2",
"vinyl": "^2.1.0", "vinyl": "^2.1.0",
"vinyl-fs": "^2.4.4", "vinyl-fs": "^2.4.4",
"webpack": "^3.6.0", "webpack": "^3.10.0",
"webpack-stream": "^4.0.0", "webpack-stream": "^4.0.0",
"wintersmith": "^2.4.1", "wintersmith": "^2.4.1",
"yargs": "^9.0.1" "yargs": "^9.0.1"

View File

@ -2001,8 +2001,10 @@ var WorkerTransport = (function WorkerTransportClosure() {
return { return {
info: results[0], info: results[0],
metadata: (results[1] ? new Metadata(results[1]) : null), metadata: (results[1] ? new Metadata(results[1]) : null),
contentDispositionFileName: (this._fullReader ?
this._fullReader.fileName : null),
}; };
}); }.bind(this));
}, },
getStats: function WorkerTransport_getStats() { getStats: function WorkerTransport_getStats() {

View File

@ -17,8 +17,8 @@ import {
AbortException, assert, createPromiseCapability AbortException, assert, createPromiseCapability
} from '../shared/util'; } from '../shared/util';
import { import {
createResponseStatusError, validateRangeRequestCapabilities, createResponseStatusError, extractFilenameFromHeader,
validateResponseStatus validateRangeRequestCapabilities, validateResponseStatus
} from './network_utils'; } from './network_utils';
function createFetchOptions(headers, withCredentials) { function createFetchOptions(headers, withCredentials) {
@ -67,6 +67,7 @@ class PDFFetchStream {
class PDFFetchStreamReader { class PDFFetchStreamReader {
constructor(stream) { constructor(stream) {
this._stream = stream; this._stream = stream;
this._fileName = null;
this._reader = null; this._reader = null;
this._loaded = 0; this._loaded = 0;
let source = stream.source; let source = stream.source;
@ -100,11 +101,13 @@ class PDFFetchStreamReader {
this._reader = response.body.getReader(); this._reader = response.body.getReader();
this._headersCapability.resolve(); this._headersCapability.resolve();
const getResponseHeader = (name) => {
return response.headers.get(name);
};
let { allowRangeRequests, suggestedLength, } = let { allowRangeRequests, suggestedLength, } =
validateRangeRequestCapabilities({ validateRangeRequestCapabilities({
getResponseHeader: (name) => { getResponseHeader,
return response.headers.get(name);
},
isHttp: this._stream.isHttp, isHttp: this._stream.isHttp,
rangeChunkSize: this._rangeChunkSize, rangeChunkSize: this._rangeChunkSize,
disableRange: this._disableRange, disableRange: this._disableRange,
@ -112,6 +115,7 @@ class PDFFetchStreamReader {
this._contentLength = suggestedLength; this._contentLength = suggestedLength;
this._isRangeSupported = allowRangeRequests; this._isRangeSupported = allowRangeRequests;
this._fileName = extractFilenameFromHeader(getResponseHeader);
// We need to stop reading when range is supported and streaming is // We need to stop reading when range is supported and streaming is
// disabled. // disabled.
@ -131,6 +135,10 @@ class PDFFetchStreamReader {
return this._contentLength; return this._contentLength;
} }
get fileName() {
return this._fileName;
}
get isRangeSupported() { get isRangeSupported() {
return this._isRangeSupported; return this._isRangeSupported;
} }

View File

@ -15,7 +15,8 @@
import { assert, createPromiseCapability, stringToBytes } from '../shared/util'; import { assert, createPromiseCapability, stringToBytes } from '../shared/util';
import { import {
createResponseStatusError, validateRangeRequestCapabilities createResponseStatusError, extractFilenameFromHeader,
validateRangeRequestCapabilities
} from './network_utils'; } from './network_utils';
import globalScope from '../shared/global_scope'; import globalScope from '../shared/global_scope';
@ -340,6 +341,7 @@ function PDFNetworkStreamFullRequestReader(manager, source) {
this._requests = []; this._requests = [];
this._done = false; this._done = false;
this._storedError = undefined; this._storedError = undefined;
this._fileName = null;
this.onProgress = null; this.onProgress = null;
} }
@ -350,11 +352,13 @@ PDFNetworkStreamFullRequestReader.prototype = {
var fullRequestXhrId = this._fullRequestId; var fullRequestXhrId = this._fullRequestId;
var fullRequestXhr = this._manager.getRequestXhr(fullRequestXhrId); var fullRequestXhr = this._manager.getRequestXhr(fullRequestXhrId);
const getResponseHeader = (name) => {
return fullRequestXhr.getResponseHeader(name);
};
let { allowRangeRequests, suggestedLength, } = let { allowRangeRequests, suggestedLength, } =
validateRangeRequestCapabilities({ validateRangeRequestCapabilities({
getResponseHeader: (name) => { getResponseHeader,
return fullRequestXhr.getResponseHeader(name);
},
isHttp: this._manager.isHttp, isHttp: this._manager.isHttp,
rangeChunkSize: this._rangeChunkSize, rangeChunkSize: this._rangeChunkSize,
disableRange: this._disableRange, disableRange: this._disableRange,
@ -381,6 +385,11 @@ PDFNetworkStreamFullRequestReader.prototype = {
networkManager.abortRequest(fullRequestXhrId); networkManager.abortRequest(fullRequestXhrId);
} }
// Content-Disposition: attachment; filename=Naïve file.txt
if (networkManager.isPendingRequest(fullRequestXhrId)) {
this._fileName = extractFilenameFromHeader(getResponseHeader);
}
this._headersReceivedCapability.resolve(); this._headersReceivedCapability.resolve();
}, },
@ -429,6 +438,10 @@ PDFNetworkStreamFullRequestReader.prototype = {
} }
}, },
get fileName() {
return this._fileName;
},
get isRangeSupported() { get isRangeSupported() {
return this._isRangeSupported; return this._isRangeSupported;
}, },

View File

@ -16,6 +16,7 @@
import { import {
assert, MissingPDFException, UnexpectedResponseException assert, MissingPDFException, UnexpectedResponseException
} from '../shared/util'; } from '../shared/util';
import { getFilenameFromUrl } from './dom_utils';
function validateRangeRequestCapabilities({ getResponseHeader, isHttp, function validateRangeRequestCapabilities({ getResponseHeader, isHttp,
rangeChunkSize, disableRange, }) { rangeChunkSize, disableRange, }) {
@ -65,8 +66,23 @@ function validateResponseStatus(status) {
return status === 200 || status === 206; return status === 200 || status === 206;
} }
function extractFilenameFromHeader(getResponseHeader) {
const contentDisposition = getResponseHeader('Content-Disposition');
if (contentDisposition) {
let parts =
/.+;\s*filename=(?:"|')(.+\.pdf)(?:"|')/gi.exec(contentDisposition);
if (parts !== null && parts.length > 1) {
return getFilenameFromUrl(parts[1]);
}
}
return null;
}
export { export {
createResponseStatusError, createResponseStatusError,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
validateResponseStatus, validateResponseStatus,
extractFilenameFromHeader,
}; };

View File

@ -22,7 +22,9 @@ let url = __non_webpack_require__('url');
import { import {
AbortException, assert, createPromiseCapability AbortException, assert, createPromiseCapability
} from '../shared/util'; } from '../shared/util';
import { validateRangeRequestCapabilities } from './network_utils'; import {
extractFilenameFromHeader, validateRangeRequestCapabilities
} from './network_utils';
const fileUriRegex = /^file:\/\/\/[a-zA-Z]:\//; const fileUriRegex = /^file:\/\/\/[a-zA-Z]:\//;
@ -74,6 +76,7 @@ class BaseFullReader {
this._done = false; this._done = false;
this._errored = false; this._errored = false;
this._reason = null; this._reason = null;
this._fileName = null;
this.onProgress = null; this.onProgress = null;
let source = stream.source; let source = stream.source;
this._contentLength = source.length; // optional this._contentLength = source.length; // optional
@ -109,6 +112,10 @@ class BaseFullReader {
return this._isStreamingSupported; return this._isStreamingSupported;
} }
get fileName() {
return this._fileName;
}
read() { read() {
return this._readCapability.promise.then(() => { return this._readCapability.promise.then(() => {
if (this._done) { if (this._done) {
@ -284,13 +291,15 @@ class PDFNodeStreamFullReader extends BaseFullReader {
this._headersCapability.resolve(); this._headersCapability.resolve();
this._setReadableStream(response); this._setReadableStream(response);
const getResponseHeader = (name) => {
// Make sure that headers name are in lower case, as mentioned
// here: https://nodejs.org/api/http.html#http_message_headers.
return this._readableStream.headers[name.toLowerCase()];
};
let { allowRangeRequests, suggestedLength, } = let { allowRangeRequests, suggestedLength, } =
validateRangeRequestCapabilities({ validateRangeRequestCapabilities({
getResponseHeader: (name) => { getResponseHeader,
// Make sure that headers name are in lower case, as mentioned
// here: https://nodejs.org/api/http.html#http_message_headers.
return this._readableStream.headers[name.toLowerCase()];
},
isHttp: stream.isHttp, isHttp: stream.isHttp,
rangeChunkSize: this._rangeChunkSize, rangeChunkSize: this._rangeChunkSize,
disableRange: this._disableRange, disableRange: this._disableRange,
@ -301,6 +310,9 @@ class PDFNodeStreamFullReader extends BaseFullReader {
} }
// Setting right content length. // Setting right content length.
this._contentLength = suggestedLength; this._contentLength = suggestedLength;
// Setting the file name from the response header
this._fileName = extractFilenameFromHeader(getResponseHeader);
}; };
this._request = null; this._request = null;

View File

@ -14,8 +14,8 @@
*/ */
import { import {
createResponseStatusError, validateRangeRequestCapabilities, createResponseStatusError, extractFilenameFromHeader,
validateResponseStatus validateRangeRequestCapabilities, validateResponseStatus
} from '../../src/display/network_utils'; } from '../../src/display/network_utils';
import { import {
MissingPDFException, UnexpectedResponseException MissingPDFException, UnexpectedResponseException
@ -175,4 +175,62 @@ describe('network_utils', function() {
expect(validateResponseStatus(undefined)).toEqual(false); expect(validateResponseStatus(undefined)).toEqual(false);
}); });
}); });
describe('extractFilenameFromHeader', function () {
it('returns null when content disposition header is blank', function() {
expect(extractFilenameFromHeader(function() {
return null;
})).toBeNull();
expect(extractFilenameFromHeader(function() {
return undefined;
})).toBeNull();
expect(extractFilenameFromHeader(function() {
return '';
})).toBeNull();
});
it('gets the filename from the response header', function () {
expect(extractFilenameFromHeader(function() {
return 'Content-Disposition: inline';
})).toBeNull();
expect(extractFilenameFromHeader(function() {
return 'Content-Disposition: attachment';
})).toBeNull();
expect(extractFilenameFromHeader(function() {
return 'Content-Disposition: attachment; filename="filename.pdf"';
})).toBe('filename.pdf');
});
it('returns null when content disposition is form-data', function () {
expect(extractFilenameFromHeader(function() {
return 'Content-Disposition: form-data';
})).toBeNull();
expect(extractFilenameFromHeader(function() {
return 'Content-Disposition: form-data; name="filename"';
})).toBeNull();
expect(extractFilenameFromHeader(function () {
return 'Content-Disposition: form-data; ' +
'name="filename"; filename="file.pdf"';
})).toBe('file.pdf');
});
it('Only extracts file names with pdf extension', function () {
expect(extractFilenameFromHeader(function() {
return 'Content-Disposition: attachment; filename="filename.png"';
})).toBeNull();
});
it('Extension validation is case insensitive', function () {
expect(extractFilenameFromHeader(function() {
return 'Content-Disposition: form-data; ' +
'name="fieldName"; filename="file.PdF"';
})).toBe('file.PdF');
});
});
}); });

View File

@ -154,6 +154,7 @@ let PDFViewerApplication = {
baseUrl: '', baseUrl: '',
externalServices: DefaultExternalServices, externalServices: DefaultExternalServices,
_boundEvents: {}, _boundEvents: {},
contentDispositionFileName: null,
// Called once when the document is loaded. // Called once when the document is loaded.
initialize(appConfig) { initialize(appConfig) {
@ -678,6 +679,7 @@ let PDFViewerApplication = {
this.downloadComplete = false; this.downloadComplete = false;
this.url = ''; this.url = '';
this.baseUrl = ''; this.baseUrl = '';
this.contentDispositionFileName = null;
this.pdfSidebar.reset(); this.pdfSidebar.reset();
this.pdfOutlineViewer.reset(); this.pdfOutlineViewer.reset();
@ -801,7 +803,8 @@ let PDFViewerApplication = {
let url = this.baseUrl; let url = this.baseUrl;
// Use this.url instead of this.baseUrl to perform filename detection based // Use this.url instead of this.baseUrl to perform filename detection based
// on the reference fragment as ultimate fallback if needed. // on the reference fragment as ultimate fallback if needed.
let filename = getPDFFileNameFromURL(this.url); let filename = this.contentDispositionFileName ||
getPDFFileNameFromURL(this.url);
let downloadManager = this.downloadManager; let downloadManager = this.downloadManager;
downloadManager.onerror = (err) => { downloadManager.onerror = (err) => {
// This error won't really be helpful because it's likely the // This error won't really be helpful because it's likely the
@ -1153,9 +1156,11 @@ let PDFViewerApplication = {
}); });
}); });
pdfDocument.getMetadata().then(({ info, metadata, }) => { pdfDocument.getMetadata().then(
({ info, metadata, contentDispositionFileName, }) => {
this.documentInfo = info; this.documentInfo = info;
this.metadata = metadata; this.metadata = metadata;
this.contentDispositionFileName = contentDispositionFileName;
// Provides some basic debug information // Provides some basic debug information
console.log('PDF ' + pdfDocument.fingerprint + ' [' + console.log('PDF ' + pdfDocument.fingerprint + ' [' +
@ -1181,6 +1186,10 @@ let PDFViewerApplication = {
this.setTitle(pdfTitle + ' - ' + document.title); this.setTitle(pdfTitle + ' - ' + document.title);
} }
if (!pdfTitle && contentDispositionFileName) {
this.setTitle(contentDispositionFileName);
}
if (info.IsAcroFormPresent) { if (info.IsAcroFormPresent) {
console.warn('Warning: AcroForm/XFA is not supported'); console.warn('Warning: AcroForm/XFA is not supported');
this.fallback(UNSUPPORTED_FEATURES.forms); this.fallback(UNSUPPORTED_FEATURES.forms);