diff --git a/src/core/worker.js b/src/core/worker.js
index 0326bf4f4..12a75df53 100644
--- a/src/core/worker.js
+++ b/src/core/worker.js
@@ -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.
diff --git a/src/display/api.js b/src/display/api.js
index 2c5b339cb..fb52474b5 100644
--- a/src/display/api.js
+++ b/src/display/api.js
@@ -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),
};
});
},
diff --git a/src/display/fetch_stream.js b/src/display/fetch_stream.js
index 9fb7e1c56..44d726c96 100644
--- a/src/display/fetch_stream.js
+++ b/src/display/fetch_stream.js
@@ -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;
}
diff --git a/src/display/network.js b/src/display/network.js
index d34843451..54a93eeed 100644
--- a/src/display/network.js
+++ b/src/display/network.js
@@ -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;
},
diff --git a/src/display/network_utils.js b/src/display/network_utils.js
index 1f4f15817..1b0eb0ee9 100644
--- a/src/display/network_utils.js
+++ b/src/display/network_utils.js
@@ -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,
};
diff --git a/src/display/node_stream.js b/src/display/node_stream.js
index baa439e03..a8431cdb7 100644
--- a/src/display/node_stream.js
+++ b/src/display/node_stream.js
@@ -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;
diff --git a/src/display/transport_stream.js b/src/display/transport_stream.js
index a89920589..21525cfac 100644
--- a/src/display/transport_stream.js
+++ b/src/display/transport_stream.js
@@ -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;
},
diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js
index d67596a31..48dd07c91 100644
--- a/test/unit/api_spec.js
+++ b/test/unit/api_spec.js
@@ -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);
diff --git a/test/unit/network_utils_spec.js b/test/unit/network_utils_spec.js
index 7cd1b848a..b98ac5d01 100644
--- a/test/unit/network_utils_spec.js
+++ b/test/unit/network_utils_spec.js
@@ -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(
diff --git a/web/app.js b/web/app.js
index 9c561f22d..edeea6ec3 100644
--- a/web/app.js
+++ b/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) {
diff --git a/web/pdf_document_properties.js b/web/pdf_document_properties.js
index 6727627b7..cb118920b 100644
--- a/web/pdf_document_properties.js
+++ b/web/pdf_document_properties.js
@@ -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,