Merge pull request #9379 from Snuffleupagus/contentDispositionFilename
[api-minor] Extract the Content-Disposition filename
This commit is contained in:
commit
75dc2bbd35
@ -103,6 +103,16 @@ IPDFStreamReader.prototype = {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the Content-Disposition filename. It is defined after the headersReady
|
||||
* promise is resolved.
|
||||
* @returns {string|null} The filename, or `null` if the Content-Disposition
|
||||
* header is missing/invalid.
|
||||
*/
|
||||
get filename() {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets PDF binary data length. It is defined after the headersReady promise
|
||||
* is resolved.
|
||||
|
@ -1997,10 +1997,12 @@ var WorkerTransport = (function WorkerTransportClosure() {
|
||||
|
||||
getMetadata: function WorkerTransport_getMetadata() {
|
||||
return this.messageHandler.sendWithPromise('GetMetadata', null).
|
||||
then(function transportMetadata(results) {
|
||||
then((results) => {
|
||||
return {
|
||||
info: results[0],
|
||||
metadata: (results[1] ? new Metadata(results[1]) : null),
|
||||
contentDispositionFilename: (this._fullReader ?
|
||||
this._fullReader.filename : null),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
@ -17,8 +17,8 @@ import {
|
||||
AbortException, assert, createPromiseCapability
|
||||
} from '../shared/util';
|
||||
import {
|
||||
createResponseStatusError, validateRangeRequestCapabilities,
|
||||
validateResponseStatus
|
||||
createResponseStatusError, extractFilenameFromHeader,
|
||||
validateRangeRequestCapabilities, validateResponseStatus
|
||||
} from './network_utils';
|
||||
|
||||
function createFetchOptions(headers, withCredentials) {
|
||||
@ -69,6 +69,7 @@ class PDFFetchStreamReader {
|
||||
this._stream = stream;
|
||||
this._reader = null;
|
||||
this._loaded = 0;
|
||||
this._filename = null;
|
||||
let source = stream.source;
|
||||
this._withCredentials = source.withCredentials;
|
||||
this._contentLength = source.length;
|
||||
@ -100,11 +101,12 @@ class PDFFetchStreamReader {
|
||||
this._reader = response.body.getReader();
|
||||
this._headersCapability.resolve();
|
||||
|
||||
const getResponseHeader = (name) => {
|
||||
return response.headers.get(name);
|
||||
};
|
||||
let { allowRangeRequests, suggestedLength, } =
|
||||
validateRangeRequestCapabilities({
|
||||
getResponseHeader: (name) => {
|
||||
return response.headers.get(name);
|
||||
},
|
||||
getResponseHeader,
|
||||
isHttp: this._stream.isHttp,
|
||||
rangeChunkSize: this._rangeChunkSize,
|
||||
disableRange: this._disableRange,
|
||||
@ -113,6 +115,8 @@ class PDFFetchStreamReader {
|
||||
this._contentLength = suggestedLength;
|
||||
this._isRangeSupported = allowRangeRequests;
|
||||
|
||||
this._filename = extractFilenameFromHeader(getResponseHeader);
|
||||
|
||||
// We need to stop reading when range is supported and streaming is
|
||||
// disabled.
|
||||
if (!this._isStreamingSupported && this._isRangeSupported) {
|
||||
@ -127,6 +131,10 @@ class PDFFetchStreamReader {
|
||||
return this._headersCapability.promise;
|
||||
}
|
||||
|
||||
get filename() {
|
||||
return this._filename;
|
||||
}
|
||||
|
||||
get contentLength() {
|
||||
return this._contentLength;
|
||||
}
|
||||
|
@ -15,7 +15,8 @@
|
||||
|
||||
import { assert, createPromiseCapability, stringToBytes } from '../shared/util';
|
||||
import {
|
||||
createResponseStatusError, validateRangeRequestCapabilities
|
||||
createResponseStatusError, extractFilenameFromHeader,
|
||||
validateRangeRequestCapabilities
|
||||
} from './network_utils';
|
||||
import globalScope from '../shared/global_scope';
|
||||
|
||||
@ -340,6 +341,7 @@ function PDFNetworkStreamFullRequestReader(manager, source) {
|
||||
this._requests = [];
|
||||
this._done = false;
|
||||
this._storedError = undefined;
|
||||
this._filename = null;
|
||||
|
||||
this.onProgress = null;
|
||||
}
|
||||
@ -350,11 +352,13 @@ PDFNetworkStreamFullRequestReader.prototype = {
|
||||
var fullRequestXhrId = this._fullRequestId;
|
||||
var fullRequestXhr = this._manager.getRequestXhr(fullRequestXhrId);
|
||||
|
||||
const getResponseHeader = (name) => {
|
||||
return fullRequestXhr.getResponseHeader(name);
|
||||
};
|
||||
|
||||
let { allowRangeRequests, suggestedLength, } =
|
||||
validateRangeRequestCapabilities({
|
||||
getResponseHeader: (name) => {
|
||||
return fullRequestXhr.getResponseHeader(name);
|
||||
},
|
||||
getResponseHeader,
|
||||
isHttp: this._manager.isHttp,
|
||||
rangeChunkSize: this._rangeChunkSize,
|
||||
disableRange: this._disableRange,
|
||||
@ -367,6 +371,8 @@ PDFNetworkStreamFullRequestReader.prototype = {
|
||||
this._isRangeSupported = true;
|
||||
}
|
||||
|
||||
this._filename = extractFilenameFromHeader(getResponseHeader);
|
||||
|
||||
var networkManager = this._manager;
|
||||
if (networkManager.isStreamingRequest(fullRequestXhrId)) {
|
||||
// We can continue fetching when progressive loading is enabled,
|
||||
@ -429,6 +435,10 @@ PDFNetworkStreamFullRequestReader.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
get filename() {
|
||||
return this._filename;
|
||||
},
|
||||
|
||||
get isRangeSupported() {
|
||||
return this._isRangeSupported;
|
||||
},
|
||||
|
@ -16,6 +16,7 @@
|
||||
import {
|
||||
assert, MissingPDFException, UnexpectedResponseException
|
||||
} from '../shared/util';
|
||||
import { getFilenameFromUrl } from './dom_utils';
|
||||
|
||||
function validateRangeRequestCapabilities({ getResponseHeader, isHttp,
|
||||
rangeChunkSize, disableRange, }) {
|
||||
@ -52,6 +53,18 @@ function validateRangeRequestCapabilities({ getResponseHeader, isHttp,
|
||||
return returnValues;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function createResponseStatusError(status, url) {
|
||||
if (status === 404 || status === 0 && /^file:/.test(url)) {
|
||||
return new MissingPDFException('Missing PDF "' + url + '".');
|
||||
@ -67,6 +80,7 @@ function validateResponseStatus(status) {
|
||||
|
||||
export {
|
||||
createResponseStatusError,
|
||||
extractFilenameFromHeader,
|
||||
validateRangeRequestCapabilities,
|
||||
validateResponseStatus,
|
||||
};
|
||||
|
@ -22,7 +22,9 @@ let url = __non_webpack_require__('url');
|
||||
import {
|
||||
AbortException, assert, createPromiseCapability
|
||||
} from '../shared/util';
|
||||
import { validateRangeRequestCapabilities } from './network_utils';
|
||||
import {
|
||||
extractFilenameFromHeader, validateRangeRequestCapabilities
|
||||
} from './network_utils';
|
||||
|
||||
const fileUriRegex = /^file:\/\/\/[a-zA-Z]:\//;
|
||||
|
||||
@ -78,6 +80,7 @@ class BaseFullReader {
|
||||
let source = stream.source;
|
||||
this._contentLength = source.length; // optional
|
||||
this._loaded = 0;
|
||||
this._filename = null;
|
||||
|
||||
this._disableRange = source.disableRange || false;
|
||||
this._rangeChunkSize = source.rangeChunkSize;
|
||||
@ -97,6 +100,10 @@ class BaseFullReader {
|
||||
return this._headersCapability.promise;
|
||||
}
|
||||
|
||||
get filename() {
|
||||
return this._filename;
|
||||
}
|
||||
|
||||
get contentLength() {
|
||||
return this._contentLength;
|
||||
}
|
||||
@ -284,23 +291,26 @@ class PDFNodeStreamFullReader extends BaseFullReader {
|
||||
this._headersCapability.resolve();
|
||||
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, } =
|
||||
validateRangeRequestCapabilities({
|
||||
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()];
|
||||
},
|
||||
isHttp: stream.isHttp,
|
||||
rangeChunkSize: this._rangeChunkSize,
|
||||
disableRange: this._disableRange,
|
||||
});
|
||||
validateRangeRequestCapabilities({
|
||||
getResponseHeader,
|
||||
isHttp: stream.isHttp,
|
||||
rangeChunkSize: this._rangeChunkSize,
|
||||
disableRange: this._disableRange,
|
||||
});
|
||||
|
||||
if (allowRangeRequests) {
|
||||
this._isRangeSupported = true;
|
||||
}
|
||||
// Setting right content length.
|
||||
this._contentLength = suggestedLength;
|
||||
|
||||
this._filename = extractFilenameFromHeader(getResponseHeader);
|
||||
};
|
||||
|
||||
this._request = null;
|
||||
|
@ -119,6 +119,7 @@ var PDFDataTransportStream = (function PDFDataTransportStreamClosure() {
|
||||
function PDFDataTransportStreamReader(stream, queuedChunks) {
|
||||
this._stream = stream;
|
||||
this._done = false;
|
||||
this._filename = null;
|
||||
this._queuedChunks = queuedChunks || [];
|
||||
this._requests = [];
|
||||
this._headersReady = Promise.resolve();
|
||||
@ -143,6 +144,10 @@ var PDFDataTransportStream = (function PDFDataTransportStreamClosure() {
|
||||
return this._headersReady;
|
||||
},
|
||||
|
||||
get filename() {
|
||||
return this._filename;
|
||||
},
|
||||
|
||||
get isRangeSupported() {
|
||||
return this._stream._isRangeSupported;
|
||||
},
|
||||
|
@ -794,6 +794,7 @@ describe('api', function() {
|
||||
expect(metadata.info['Title']).toEqual('Basic API Test');
|
||||
expect(metadata.info['PDFFormatVersion']).toEqual('1.7');
|
||||
expect(metadata.metadata.get('dc:title')).toEqual('Basic API Test');
|
||||
expect(metadata.contentDispositionFilename).toEqual(null);
|
||||
done();
|
||||
}).catch(function (reason) {
|
||||
done.fail(reason);
|
||||
|
@ -14,8 +14,8 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
createResponseStatusError, validateRangeRequestCapabilities,
|
||||
validateResponseStatus
|
||||
createResponseStatusError, extractFilenameFromHeader,
|
||||
validateRangeRequestCapabilities, validateResponseStatus
|
||||
} from '../../src/display/network_utils';
|
||||
import {
|
||||
MissingPDFException, UnexpectedResponseException
|
||||
@ -134,6 +134,84 @@ describe('network_utils', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractFilenameFromHeader', function() {
|
||||
it('returns null when content disposition header is blank', function() {
|
||||
expect(extractFilenameFromHeader((headerName) => {
|
||||
if (headerName === 'Content-Disposition') {
|
||||
return null;
|
||||
}
|
||||
})).toBeNull();
|
||||
|
||||
expect(extractFilenameFromHeader((headerName) => {
|
||||
if (headerName === 'Content-Disposition') {
|
||||
return undefined;
|
||||
}
|
||||
})).toBeNull();
|
||||
|
||||
expect(extractFilenameFromHeader((headerName) => {
|
||||
if (headerName === 'Content-Disposition') {
|
||||
return '';
|
||||
}
|
||||
})).toBeNull();
|
||||
});
|
||||
|
||||
it('gets the filename from the response header', function() {
|
||||
expect(extractFilenameFromHeader((headerName) => {
|
||||
if (headerName === 'Content-Disposition') {
|
||||
return 'inline';
|
||||
}
|
||||
})).toBeNull();
|
||||
|
||||
expect(extractFilenameFromHeader((headerName) => {
|
||||
if (headerName === 'Content-Disposition') {
|
||||
return 'attachment';
|
||||
}
|
||||
})).toBeNull();
|
||||
|
||||
expect(extractFilenameFromHeader((headerName) => {
|
||||
if (headerName === 'Content-Disposition') {
|
||||
return 'attachment; filename="filename.pdf"';
|
||||
}
|
||||
})).toEqual('filename.pdf');
|
||||
});
|
||||
|
||||
it('returns null when content disposition is form-data', function() {
|
||||
expect(extractFilenameFromHeader((headerName) => {
|
||||
if (headerName === 'Content-Disposition') {
|
||||
return 'form-data';
|
||||
}
|
||||
})).toBeNull();
|
||||
|
||||
expect(extractFilenameFromHeader((headerName) => {
|
||||
if (headerName === 'Content-Disposition') {
|
||||
return 'form-data; name="filename.pdf"';
|
||||
}
|
||||
})).toBeNull();
|
||||
|
||||
expect(extractFilenameFromHeader((headerName) => {
|
||||
if (headerName === 'Content-Disposition') {
|
||||
return 'form-data; name="filename.pdf"; filename="file.pdf"';
|
||||
}
|
||||
})).toEqual('file.pdf');
|
||||
});
|
||||
|
||||
it('only extracts filename with pdf extension', function () {
|
||||
expect(extractFilenameFromHeader((headerName) => {
|
||||
if (headerName === 'Content-Disposition') {
|
||||
return 'attachment; filename="filename.png"';
|
||||
}
|
||||
})).toBeNull();
|
||||
});
|
||||
|
||||
it('extension validation is case insensitive', function () {
|
||||
expect(extractFilenameFromHeader((headerName) => {
|
||||
if (headerName === 'Content-Disposition') {
|
||||
return 'form-data; name="fieldName"; filename="file.PdF"';
|
||||
}
|
||||
})).toEqual('file.PdF');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createResponseStatusError', function() {
|
||||
it('handles missing PDF file responses', function() {
|
||||
expect(createResponseStatusError(404, 'https://foo.com/bar.pdf')).toEqual(
|
||||
|
14
web/app.js
14
web/app.js
@ -154,6 +154,7 @@ let PDFViewerApplication = {
|
||||
baseUrl: '',
|
||||
externalServices: DefaultExternalServices,
|
||||
_boundEvents: {},
|
||||
contentDispositionFilename: null,
|
||||
|
||||
// Called once when the document is loaded.
|
||||
initialize(appConfig) {
|
||||
@ -678,6 +679,7 @@ let PDFViewerApplication = {
|
||||
this.downloadComplete = false;
|
||||
this.url = '';
|
||||
this.baseUrl = '';
|
||||
this.contentDispositionFilename = null;
|
||||
|
||||
this.pdfSidebar.reset();
|
||||
this.pdfOutlineViewer.reset();
|
||||
@ -801,7 +803,8 @@ let PDFViewerApplication = {
|
||||
let url = this.baseUrl;
|
||||
// Use this.url instead of this.baseUrl to perform filename detection based
|
||||
// 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;
|
||||
downloadManager.onerror = (err) => {
|
||||
// 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.metadata = metadata;
|
||||
this.contentDispositionFilename = contentDispositionFilename;
|
||||
|
||||
// Provides some basic debug information
|
||||
console.log('PDF ' + pdfDocument.fingerprint + ' [' +
|
||||
@ -1178,7 +1183,10 @@ let PDFViewerApplication = {
|
||||
}
|
||||
|
||||
if (pdfTitle) {
|
||||
this.setTitle(pdfTitle + ' - ' + document.title);
|
||||
this.setTitle(
|
||||
`${pdfTitle} - ${contentDispositionFilename || document.title}`);
|
||||
} else if (contentDispositionFilename) {
|
||||
this.setTitle(contentDispositionFilename);
|
||||
}
|
||||
|
||||
if (info.IsAcroFormPresent) {
|
||||
|
@ -71,24 +71,26 @@ class PDFDocumentProperties {
|
||||
return;
|
||||
}
|
||||
// Get the document properties.
|
||||
this.pdfDocument.getMetadata().then(({ info, metadata, }) => {
|
||||
this.pdfDocument.getMetadata().then(
|
||||
({ info, metadata, contentDispositionFilename, }) => {
|
||||
return Promise.all([
|
||||
info,
|
||||
metadata,
|
||||
contentDispositionFilename || getPDFFileNameFromURL(this.url),
|
||||
this._parseFileSize(this.maybeFileSize),
|
||||
this._parseDate(info.CreationDate),
|
||||
this._parseDate(info.ModDate)
|
||||
]);
|
||||
}).then(([info, metadata, fileSize, creationDate, modificationDate]) => {
|
||||
}).then(([info, metadata, fileName, fileSize, creationDate, modDate]) => {
|
||||
freezeFieldData({
|
||||
'fileName': getPDFFileNameFromURL(this.url),
|
||||
'fileName': fileName,
|
||||
'fileSize': fileSize,
|
||||
'title': info.Title,
|
||||
'author': info.Author,
|
||||
'subject': info.Subject,
|
||||
'keywords': info.Keywords,
|
||||
'creationDate': creationDate,
|
||||
'modificationDate': modificationDate,
|
||||
'modificationDate': modDate,
|
||||
'creator': info.Creator,
|
||||
'producer': info.Producer,
|
||||
'version': info.PDFFormatVersion,
|
||||
|
Loading…
Reference in New Issue
Block a user