Skip to content

Commit b97cee2

Browse files
committed
Correctly handle files with a hash sign in their names (bug 1894166)
It fixes #19990.
1 parent e9527ce commit b97cee2

6 files changed

Lines changed: 112 additions & 21 deletions

File tree

src/display/display_utils.js

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -338,31 +338,82 @@ function getPdfFilenameFromUrl(url, defaultFilename = "document.pdf") {
338338
warn('getPdfFilenameFromUrl: ignore "data:"-URL for performance reasons.');
339339
return defaultFilename;
340340
}
341-
const reURI = /^(?:(?:[^:]+:)?\/\/[^/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;
342-
// SCHEME HOST 1.PATH 2.QUERY 3.REF
343-
// Pattern to get last matching NAME.pdf
344-
const reFilename = /[^/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
345-
const splitURI = reURI.exec(url);
346-
let suggestedFilename =
347-
reFilename.exec(splitURI[1]) ||
348-
reFilename.exec(splitURI[2]) ||
349-
reFilename.exec(splitURI[3]);
350-
if (suggestedFilename) {
351-
suggestedFilename = suggestedFilename[0];
352-
if (suggestedFilename.includes("%")) {
353-
// URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf
341+
342+
const getURL = urlString => {
343+
try {
344+
return new URL(urlString);
345+
} catch {
354346
try {
355-
suggestedFilename = reFilename.exec(
356-
decodeURIComponent(suggestedFilename)
357-
)[0];
347+
return new URL(decodeURIComponent(urlString));
358348
} catch {
359-
// Possible (extremely rare) errors:
360-
// URIError "Malformed URI", e.g. for "%AA.pdf"
361-
// TypeError "null has no properties", e.g. for "%2F.pdf"
349+
try {
350+
// Attempt to parse the URL using the document's base URI.
351+
return new URL(urlString, "https://foo.bar");
352+
} catch {
353+
try {
354+
return new URL(decodeURIComponent(urlString), "https://foo.bar");
355+
} catch {
356+
return null;
357+
}
358+
}
359+
}
360+
}
361+
};
362+
363+
const newURL = getURL(url);
364+
if (!newURL) {
365+
// If the URL is invalid, return the default filename.
366+
return defaultFilename;
367+
}
368+
369+
const decode = name => {
370+
try {
371+
let decoded = decodeURIComponent(name);
372+
if (decoded.includes("/")) {
373+
decoded = decoded.split("/").at(-1);
374+
if (decoded.test(/^\.pdf$/i)) {
375+
return decoded;
376+
}
377+
return name;
378+
}
379+
return decoded;
380+
} catch {
381+
return name;
382+
}
383+
};
384+
385+
const pdfRegex = /\.pdf$/i;
386+
const filename = newURL.pathname.split("/").at(-1);
387+
if (pdfRegex.test(filename)) {
388+
return decode(filename);
389+
}
390+
391+
if (newURL.searchParams.size > 0) {
392+
const values = Array.from(newURL.searchParams.values()).reverse();
393+
for (const value of values) {
394+
if (pdfRegex.test(value)) {
395+
// If any of the search parameters ends with ".pdf", return it.
396+
return decode(value);
397+
}
398+
}
399+
const keys = Array.from(newURL.searchParams.keys()).reverse();
400+
for (const key of keys) {
401+
if (pdfRegex.test(key)) {
402+
// If any of the search parameter keys ends with ".pdf", return it.
403+
return decode(key);
362404
}
363405
}
364406
}
365-
return suggestedFilename || defaultFilename;
407+
408+
if (newURL.hash) {
409+
const reFilename = /[^/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
410+
const hashFilename = reFilename.exec(newURL.hash);
411+
if (hashFilename) {
412+
return decode(hashFilename[0]);
413+
}
414+
}
415+
416+
return defaultFilename;
366417
}
367418

368419
class StatTimer {

test/integration/viewer_spec.mjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,4 +1220,28 @@ describe("PDF viewer", () => {
12201220
);
12211221
});
12221222
});
1223+
1224+
describe("Filename with a hash sign", () => {
1225+
let pages;
1226+
1227+
beforeEach(async () => {
1228+
pages = await loadAndWait("empty%23hash.pdf", ".textLayer .endOfContent");
1229+
});
1230+
1231+
afterEach(async () => {
1232+
await closePages(pages);
1233+
});
1234+
1235+
it("must extract the filename correctly", async () => {
1236+
await Promise.all(
1237+
pages.map(async ([browserName, page]) => {
1238+
const filename = await page.evaluate(() => document.title);
1239+
1240+
expect(filename)
1241+
.withContext(`In ${browserName}`)
1242+
.toBe("empty#hash.pdf");
1243+
})
1244+
);
1245+
});
1246+
});
12231247
});

test/pdfs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,3 +726,4 @@
726726
!chrome-text-selection-markedContent.pdf
727727
!bug1963407.pdf
728728
!issue19517.pdf
729+
!empty#hash.pdf

test/pdfs/empty#hash.pdf

4.8 KB
Binary file not shown.

test/unit/display_utils_spec.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,20 @@ describe("display_utils", function () {
193193
"document.pdf"
194194
);
195195
});
196+
197+
it("gets PDF filename with a hash sign", function () {
198+
expect(getPdfFilenameFromUrl("/foo.html?file=foo%23.pdf")).toEqual(
199+
"foo#.pdf"
200+
);
201+
202+
expect(getPdfFilenameFromUrl("/foo.html?file=%23.pdf")).toEqual("#.pdf");
203+
204+
expect(getPdfFilenameFromUrl("/foo.html?foo%23.pdf")).toEqual("foo#.pdf");
205+
206+
expect(getPdfFilenameFromUrl("/foo%23.pdf?a=b#c")).toEqual("foo#.pdf");
207+
208+
expect(getPdfFilenameFromUrl("foo.html#%23.pdf")).toEqual("#.pdf");
209+
});
196210
});
197211

198212
describe("isValidFetchUrl", function () {

web/app.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,7 @@ const PDFViewerApplication = {
727727
const queryString = document.location.search.substring(1);
728728
const params = parseQueryString(queryString);
729729
file = params.get("file") ?? AppOptions.get("defaultUrl");
730+
file = encodeURIComponent(file).replaceAll("%2F", "/");
730731
validateFileURL(file);
731732
} else if (PDFJSDev.test("MOZCENTRAL")) {
732733
file = window.location.href;
@@ -2336,7 +2337,7 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
23362337

23372338
this.open({
23382339
url: URL.createObjectURL(file),
2339-
originalUrl: file.name,
2340+
originalUrl: encodeURIComponent(file.name),
23402341
});
23412342
};
23422343

0 commit comments

Comments
 (0)