From b03281de1819e2e45e9e5472fc4cc43a7a6a22d9 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 11 Dec 2021 16:23:29 +0100 Subject: [PATCH 1/2] Move the permissions handling into the `BaseViewer` (PR 11789 follow-up) Besides making the permissions-functionality directly available in the viewer-components, these changes are also necessary for the next patch. --- web/app.js | 35 +---------------------------------- web/base_viewer.js | 36 +++++++++++++++++++++++++++++------- web/pdf_viewer.css | 5 +++++ web/viewer.css | 5 ----- 4 files changed, 35 insertions(+), 46 deletions(-) diff --git a/web/app.js b/web/app.js index 704ac1b95..451151b86 100644 --- a/web/app.js +++ b/web/app.js @@ -51,7 +51,6 @@ import { MissingPDFException, OPS, PDFWorker, - PermissionFlag, shadow, UnexpectedResponseException, UNSUPPORTED_FEATURES, @@ -82,7 +81,6 @@ import { ViewHistory } from "./view_history.js"; const DISABLE_AUTO_FETCH_LOADING_BAR_TIMEOUT = 5000; // ms const FORCE_PAGES_LOADED_TIMEOUT = 10000; // ms const WHEEL_ZOOM_DISABLED_TIMEOUT = 1000; // ms -const ENABLE_PERMISSIONS_CLASS = "enablePermissions"; const ViewOnLoad = { UNKNOWN: -1, @@ -530,6 +528,7 @@ const PDFViewerApplication = { enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"), + enablePermissions: AppOptions.get("enablePermissions"), }); pdfRenderingQueue.setViewer(this.pdfViewer); pdfLinkService.setViewer(this.pdfViewer); @@ -841,7 +840,6 @@ const PDFViewerApplication = { this.pdfLinkService.setDocument(null); this.pdfDocumentProperties.setDocument(null); } - webViewerResetPermissions(); this.pdfLinkService.externalLinkEnabled = true; this._fellback = false; this.store = null; @@ -1326,10 +1324,6 @@ const PDFViewerApplication = { pdfViewer.focus(); } - // Currently only the "copy"-permission is supported, hence we delay - // the `getPermissions` API call until *after* rendering has started. - this._initializePermissions(pdfDocument); - // For documents with different page sizes, once all pages are // resolved, ensure that the correct location becomes visible on load. // (To reduce the risk, in very large and/or slow loading documents, @@ -1709,24 +1703,6 @@ const PDFViewerApplication = { } }, - /** - * @private - */ - async _initializePermissions(pdfDocument) { - const permissions = await pdfDocument.getPermissions(); - - if (pdfDocument !== this.pdfDocument) { - return; // The document was closed while the permissions resolved. - } - if (!permissions || !AppOptions.get("enablePermissions")) { - return; - } - // Currently only the "copy"-permission is supported. - if (!permissions.includes(PermissionFlag.COPY)) { - this.appConfig.viewerContainer.classList.add(ENABLE_PERMISSIONS_CLASS); - } - }, - /** * @private */ @@ -2335,15 +2311,6 @@ function webViewerOpenFileViaURL(file) { } } -function webViewerResetPermissions() { - const { appConfig } = PDFViewerApplication; - if (!appConfig) { - return; - } - // Currently only the "copy"-permission is supported. - appConfig.viewerContainer.classList.remove(ENABLE_PERMISSIONS_CLASS); -} - function webViewerPageRendered({ pageNumber, error }) { // If the page is still visible when it has finished rendering, // ensure that the page number input loading indicator is hidden. diff --git a/web/base_viewer.js b/web/base_viewer.js index 8fac5d0ce..2546edca5 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -16,6 +16,7 @@ import { AnnotationMode, createPromiseCapability, + PermissionFlag, PixelsPerInch, version, } from "pdfjs-lib"; @@ -53,6 +54,7 @@ import { TextLayerBuilder } from "./text_layer_builder.js"; import { XfaLayerBuilder } from "./xfa_layer_builder.js"; const DEFAULT_CACHE_SIZE = 10; +const ENABLE_PERMISSIONS_CLASS = "enablePermissions"; const PagesCountLimit = { FORCE_SCROLL_MODE_PAGE: 15000, @@ -95,6 +97,8 @@ const PagesCountLimit = { * total pixels, i.e. width * height. Use -1 for no limit. The default value * is 4096 * 4096 (16 mega-pixels). * @property {IL10n} l10n - Localization service. + * @property {boolean} [enablePermissions] - Enables PDF document permissions, + * when they exist. The default value is `false`. */ class PDFPageViewBuffer { @@ -171,6 +175,8 @@ class PDFPageViewBuffer { class BaseViewer { #buffer = null; + #enablePermissions = false; + #previousContainerHeight = 0; #scrollModePageState = null; @@ -227,6 +233,7 @@ class BaseViewer { this.useOnlyCssZoom = options.useOnlyCssZoom || false; this.maxCanvasPixels = options.maxCanvasPixels; this.l10n = options.l10n || NullL10n; + this.#enablePermissions = options.enablePermissions || false; this.defaultRenderingQueue = !options.renderingQueue; if (this.defaultRenderingQueue) { @@ -472,10 +479,20 @@ class BaseViewer { return this.pdfDocument ? this._pagesCapability.promise : null; } - /** - * @private - */ - _onePageRenderedOrForceFetch() { + #initializePermissions(permissions, pdfDocument) { + if (pdfDocument !== this.pdfDocument) { + return; // The document was closed while the permissions resolved. + } + if (!permissions || !this.#enablePermissions) { + return; + } + // Currently only the "copy"-permission is supported. + if (!permissions.includes(PermissionFlag.COPY)) { + this.viewer.classList.add(ENABLE_PERMISSIONS_CLASS); + } + } + + #onePageRenderedOrForceFetch() { // Unless the viewer *and* its pages are visible, rendering won't start and // `this._onePageRenderedCapability` thus won't be resolved. // To ensure that automatic printing, on document load, still works even in @@ -520,6 +537,7 @@ class BaseViewer { const firstPagePromise = pdfDocument.getPage(1); // Rendering (potentially) depends on this, hence fetching it immediately. const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig(); + const permissionsPromise = pdfDocument.getPermissions(); // Given that browsers don't handle huge amounts of DOM-elements very well, // enforce usage of PAGE-scrolling when loading *very* long/large documents. @@ -564,10 +582,11 @@ class BaseViewer { // Fetch a single page so we can get a viewport that will be the default // viewport for all pages - firstPagePromise - .then(firstPdfPage => { + Promise.all([firstPagePromise, permissionsPromise]) + .then(([firstPdfPage, permissions]) => { this._firstPageCapability.resolve(firstPdfPage); this._optionalContentConfigPromise = optionalContentConfigPromise; + this.#initializePermissions(permissions, pdfDocument); const viewerElement = this._scrollMode === ScrollMode.PAGE ? null : this.viewer; @@ -626,7 +645,7 @@ class BaseViewer { // Fetch all the pages since the viewport is needed before printing // starts to create the correct size canvas. Wait until one page is // rendered so we don't tie up too many resources early on. - this._onePageRenderedOrForceFetch().then(async () => { + this.#onePageRenderedOrForceFetch().then(async () => { if (this.findController) { this.findController.setDocument(pdfDocument); // Enable searching. } @@ -750,6 +769,9 @@ class BaseViewer { this.viewer.textContent = ""; // ... and reset the Scroll mode CSS class(es) afterwards. this._updateScrollMode(); + + // Reset all PDF document permissions. + this.viewer.classList.remove(ENABLE_PERMISSIONS_CLASS); } #ensurePageViewVisible() { diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index d98a17969..01c9cfa2d 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -141,6 +141,11 @@ background: none; } +.pdfViewer.enablePermissions .textLayer span { + user-select: none !important; + cursor: not-allowed; +} + .pdfPresentationMode .pdfViewer { padding-bottom: 0; } diff --git a/web/viewer.css b/web/viewer.css index a8810c528..0541efd3b 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -204,11 +204,6 @@ select { display: none !important; } -.pdfViewer.enablePermissions .textLayer span { - user-select: none !important; - cursor: not-allowed; -} - #viewerContainer.pdfPresentationMode:fullscreen { top: 0; background-color: rgba(0, 0, 0, 1); From b1d3e7f1217b2b7281728b1f3d48199003a9ae83 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 11 Dec 2021 16:53:59 +0100 Subject: [PATCH 2/2] Support disabling of form editing when `pdfjs.enablePermissions` is set (issue 14356) For encrypted PDF documents without the required permissions set, this patch adds support for disabling of form editing. However, please note that it also requires that the `pdfjs.enablePermissions` preference is set to `true`[1] (since PDF document permissions could be seen as user hostile). Based on https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf#G6.1942134, this condition hopefully makes sense. --- [1] Either manually with `about:config`, or using e.g. a [Group Policy](https://github.com/mozilla/policy-templates). --- web/base_viewer.js | 32 +++++++++++++++++++++++++++----- web/pdf_page_view.js | 12 +++++++----- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/web/base_viewer.js b/web/base_viewer.js index 2546edca5..6c6e381e3 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -175,6 +175,10 @@ class PDFPageViewBuffer { class BaseViewer { #buffer = null; + #annotationMode = AnnotationMode.ENABLE_FORMS; + + #previousAnnotationMode = null; + #enablePermissions = false; #previousContainerHeight = 0; @@ -225,7 +229,7 @@ class BaseViewer { this._scriptingManager = options.scriptingManager || null; this.removePageBorders = options.removePageBorders || false; this.textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE; - this._annotationMode = + this.#annotationMode = options.annotationMode ?? AnnotationMode.ENABLE_FORMS; this.imageResourcesPath = options.imageResourcesPath || ""; this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; @@ -286,7 +290,7 @@ class BaseViewer { * @type {boolean} */ get renderForms() { - return this._annotationMode === AnnotationMode.ENABLE_FORMS; + return this.#annotationMode === AnnotationMode.ENABLE_FORMS; } /** @@ -479,6 +483,9 @@ class BaseViewer { return this.pdfDocument ? this._pagesCapability.promise : null; } + /** + * Currently only *some* permissions are supported. + */ #initializePermissions(permissions, pdfDocument) { if (pdfDocument !== this.pdfDocument) { return; // The document was closed while the permissions resolved. @@ -486,10 +493,20 @@ class BaseViewer { if (!permissions || !this.#enablePermissions) { return; } - // Currently only the "copy"-permission is supported. + if (!permissions.includes(PermissionFlag.COPY)) { this.viewer.classList.add(ENABLE_PERMISSIONS_CLASS); } + + if ( + !permissions.includes(PermissionFlag.MODIFY_ANNOTATIONS) && + !permissions.includes(PermissionFlag.FILL_INTERACTIVE_FORMS) + ) { + if (this.#annotationMode === AnnotationMode.ENABLE_FORMS) { + this.#previousAnnotationMode = this.#annotationMode; // Allow resetting. + this.#annotationMode = AnnotationMode.ENABLE; + } + } } #onePageRenderedOrForceFetch() { @@ -599,7 +616,7 @@ class BaseViewer { ? this : null; const annotationLayerFactory = - this._annotationMode !== AnnotationMode.DISABLE ? this : null; + this.#annotationMode !== AnnotationMode.DISABLE ? this : null; const xfaLayerFactory = isPureXfa ? this : null; for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) { @@ -614,7 +631,7 @@ class BaseViewer { textLayerFactory, textLayerMode: this.textLayerMode, annotationLayerFactory, - annotationMode: this._annotationMode, + annotationMode: this.#annotationMode, xfaLayerFactory, textHighlighterFactory: this, structTreeLayerFactory: this, @@ -772,6 +789,11 @@ class BaseViewer { // Reset all PDF document permissions. this.viewer.classList.remove(ENABLE_PERMISSIONS_CLASS); + + if (this.#previousAnnotationMode !== null) { + this.#annotationMode = this.#previousAnnotationMode; + this.#previousAnnotationMode = null; + } } #ensurePageViewVisible() { diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index ac669892b..c103451c8 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -76,6 +76,8 @@ const MAX_CANVAS_PIXELS = compatibilityParams.maxCanvasPixels || 16777216; * @implements {IRenderableView} */ class PDFPageView { + #annotationMode = AnnotationMode.ENABLE_FORMS; + /** * @param {PDFPageViewOptions} options */ @@ -96,7 +98,7 @@ class PDFPageView { options.optionalContentConfigPromise || null; this.hasRestrictedScaling = false; this.textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE; - this._annotationMode = + this.#annotationMode = options.annotationMode ?? AnnotationMode.ENABLE_FORMS; this.imageResourcesPath = options.imageResourcesPath || ""; this.useOnlyCssZoom = options.useOnlyCssZoom || false; @@ -597,7 +599,7 @@ class PDFPageView { this.textLayer = textLayer; if ( - this._annotationMode !== AnnotationMode.DISABLE && + this.#annotationMode !== AnnotationMode.DISABLE && this.annotationLayerFactory ) { this._annotationCanvasMap ||= new Map(); @@ -607,7 +609,7 @@ class PDFPageView { pdfPage, /* annotationStorage = */ null, this.imageResourcesPath, - this._annotationMode === AnnotationMode.ENABLE_FORMS, + this.#annotationMode === AnnotationMode.ENABLE_FORMS, this.l10n, /* enableScripting = */ null, /* hasJSActionsPromise = */ null, @@ -835,7 +837,7 @@ class PDFPageView { canvasContext: ctx, transform, viewport: this.viewport, - annotationMode: this._annotationMode, + annotationMode: this.#annotationMode, optionalContentConfigPromise: this._optionalContentConfigPromise, annotationCanvasMap: this._annotationCanvasMap, }; @@ -892,7 +894,7 @@ class PDFPageView { }); const promise = pdfPage .getOperatorList({ - annotationMode: this._annotationMode, + annotationMode: this.#annotationMode, }) .then(opList => { ensureNotCancelled();