stephanrauh / ngx-extended-pdf-viewer

A full-blown PDF viewer for Angular 16, 17, and beyond
https://pdfviewer.net
Apache License 2.0
482 stars 182 forks source link

Javascript errors after printing aborted #2227

Closed webarp closed 7 months ago

webarp commented 7 months ago

Hi Stephan,

I've noticed 2 JavaScript errors in the console if I try to print a document but cancel it.

After clicking on print, a small modal window with a progress bar shows up. If I click on cancel or cancel it with escape, I get an error:

pdf1

Another issues I have seen, it's when there are multiple pdf files.

I habe a table with multiple files. I click on one and open the pdf in the viewer, I click on print and the modal window with the progress bar opens. Afterwards the printing dialog opens. So far so good.

I close it and click on another link from the table to open another pdf file. It opens but when I try to print it, I get a JavaScript error saying the "The overlay does not exist".

pdf2

The modal window with the progress bar doesn't show up anymore but after a few seconds the printing dialog opens.

Is there a way to fix this?

Thank you very much for your time and for the great job.

Best Regards

stephanrauh commented 7 months ago

The first bug is not a bug. At least, that's the technical point of view. It's a promise that has been rejected, and I don't know how to tell a "legally" rejected promise from a real error. So until someone teaches me how to do it, I prefer to err on the safe side. If it annoys you (and if you're not the one to teach me), you can use the log filtering feature to get rid of the message: https://pdfviewer.net/extended-pdf-viewer/filtering-console-log

The second bug may have something to do with showing multiple PDF files on the same page. That's the same problem as displaying two PDF files side-by-side. pdf.js pollutes the global namespace, and I suspect that's what you see.

A workaround is to use *ngIf to make sure only one PDF file is shown at any point of time. Plus, add a timeout between hiding and showing the PDF file, because pdf.js is heavily asynchronous and takes some time to tidy up.

stephanrauh commented 7 months ago

I'm closing the ticket now because I believe my hints help you. If I'm wrong, don't hesitate to add comments below.

webarp commented 7 months ago

Hi Stephan,

thank you for the clarifications.

I added the log filtering function but it doesn't seem to quite work.

It shows a log when the page loads but when I get the cancel print error, the function did not trigger.

pdfviewer

Also, the pdf loading timeout didn't bring anything.

I still get the "the overlay does not exist" error.

Thank you very much from your time.

Best Regards

stephanrauh commented 7 months ago

Can you send me a reproducer for the "overlay does not exist" bug?

webarp commented 7 months ago

Hi Stephan,

I'll try to explain what I'm doing.

I have a table with documents. Once I click on a link, I make an API request, I get a blob and send it to a subcomponent where I load the viewer.

pdfviewer

I click on print as many times as I want and I can see the dialog window with the progress bar. It disappears when it's 100% and the standard printing dialog opens.

pdfviewer2

I don't open 2 viewers at once. Everything works perfectly as long as I don't load the pdfviewer again.

If I click on another section in the app and I open the same document in pdf viewer and try to print it again, it doesn't find the dialog window with progress bar anymore and it throws the error:

async open(e) {
                if (!this.#G.has(e))
                    throw new Error("The overlay does not exist.");
                if (this.#K) {
                    if (this.#K === e)
                        throw new Error("The overlay is already active.");
                    if (!this.#G.get(e).canForceClose)
                        throw new Error("Another overlay is currently active.");
                    await this.close()
                }
                this.#K = e;
                e.showModal();
                e.classList.remove("hidden")
            }

What I guess is once the dialog disappeard, it shows up again only if the viewer is still loaded. Somehow, if the viewer it's gone and I load it again, it cannot find the dialog anymore.

I get the progress dialog as many times as I want as long as the viewer it's loaded. Once I'm in another section where come back, it doesn't find the dialog anymore.

Also without the dialog, the standard printing window comes, but after a while. It takes longer as usual and if it's a big document, you may believe that printing on the print button didn't actually work, because there is no progress dialog.

That's what I see in the DOM.

When I load the component, the dialog is in the DOM

dialog1

I click on print. As long as the progress runs, the dialog tag gets an "open" attribute.

dialog2

Once it's 100% loaded, the dialog dissapears and the dialog tag gets a hidden class.

dialog3

So far so good.

But if I move to another section where I don't have the viewer loaded and I go back, even if I still see the dialog in the DOM

dialog4

If I click on print, it doesn't get the "open" attribute anymore, it throws the overlay error and after I print, I can see the hidden class.

dialog5

That "open" attribute or state or whatever it is :) causes some troubles.

But it doesn't get triggered if you come back to a section where you load the pdf viewer. I have to refresh the browser to get the dialog again.

It may need a sort of reset in ngOnDestroy or something similar, because Angular remembers some states even if the viewer is not in the DOM anymore and once you go back, it doesn't work properly anymore.

:(

stephanrauh commented 6 months ago

Sorry for answering late - I was busy during the last two weeks.

Debugging gets easier if you set the attribute minifiedJSLibraries="false". Then you can see the proper source code of the offending method:

  /**
   * @param {HTMLDialogElement} dialog - The overlay's DOM element.
   * @returns {Promise} A promise that is resolved when the overlay has been
   *                    opened.
   */
  async open(dialog) {
    if (!this.#overlays.has(dialog)) {
      throw new Error("The overlay does not exist.");
    } else if (this.#active) {
      if (this.#active === dialog) {
        throw new Error("The overlay is already active.");
      } else if (this.#overlays.get(dialog).canForceClose) {
        await this.close();
      } else {
        throw new Error("Another overlay is currently active.");
      }
    }
    this.#active = dialog;
    dialog.showModal();
    dialog.classList.remove("hidden"); // #1434 remove "hidden" class when opening the dialog for the second time
  }

I can't reproduce the bug, so I need your help. Maybe you can find out why the dialog is missing in the #overlays array. My best guess is that the asynchronous nature of pdf.js causes the problem. I've often seen that the new instance of the viewer is still being destroyed while the new instance already initializes. It's using global variables, so this can't go well.

However, you said you already added a timeout, so maybe it's a different problem.

You can also put the viewers into different iFrames. iFrames have their own set of global variables, so your problem is solved.

webarp commented 6 months ago

Hi Stephan,

I cound't find a way to fix it.

I removed the progress dialog completely because in the standard print window that comes afterwards, the users have to wait anyway again until the pdf loads.

With the progressbar window, they have to wait twice.

I added my own print button and I open directly the standard print window.

:)

stephanrauh commented 6 months ago

That makes sense, and I guess it's a clever work-around. Thanks for updating me!

Best regards, Stephan

webarp commented 6 months ago

Hi Stephan,

it doesn't seem to work in Chrome.

I'm using a function to load the blob in an iframe but Chrome wants to download the PDF so it doesn't open it in the iframe:

printPdf() {
        let blobURL = URL.createObjectURL(this.vm.reportPdf);

        let iframe = document.createElement('iframe'); //load content in an iframe to print later
        document.body.appendChild(iframe);

        iframe.style.display = 'none';
        iframe.src = blobURL;
        iframe.onload = function () {
            setTimeout(function () {
                iframe.focus();
                iframe.contentWindow.print();
            }, 1);
        };
    }

Refused to frame 'blob...' because it violates the following Content Security Policy directive: "frame-src 'self'

Do you have a tip about how to open the standard printing window without getting this error?

Best regards, Max

stephanrauh commented 6 months ago

No, I don't have a tip, but I've asked ChatGPT, and even though I can't guarantee the answer is correct, it doesn't seem to be entirely wrong. Maybe it helps!

The issue you're encountering with the iframe refusing to display the blob URL is due to the Content Security Policy (CSP) that is likely set on your web server. CSP is a security measure that helps to detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks. The error message suggests that your CSP is configured to only allow framing content from the same origin ('self').

Here are some steps and tips to resolve the issue:

  1. Modify Content Security Policy: If you have control over the CSP settings, you can modify them to allow blob URLs in frame-src. You could add blob: to your CSP directive like so:
Content-Security-Policy: frame-src 'self' blob:;

This allows blob URLs to be used in frames while maintaining the rest of the CSP policy intact.

  1. Open PDF in a New Window: If modifying the CSP isn't possible or desirable, another straightforward solution is to avoid using an iframe altogether and instead open the blob URL in a new browser tab or window. This can be achieved by changing the way you handle the blob URL:
printPdf() {
    let blobURL = URL.createObjectURL(this.vm.reportPdf);
    window.open(blobURL, '_blank').focus();
}

This method opens the PDF in a new tab, and users can use the browser’s built-in print function to print the PDF.

  1. Using a Server-Side Solution: If the PDFs are generated or can be temporarily stored server-side, consider serving them directly from a URL path that complies with your CSP. The server can set the Content-Disposition header to inline to ensure the browser attempts to display the PDF inline instead of downloading it:
    Content-Disposition: inline; filename="filename.pdf"
  1. Review and Test CSP Settings: Always ensure that your CSP settings do not inadvertently weaken your site’s security. Test your CSP changes thoroughly to ensure that they do not expose your site to XSS or other vulnerabilities.

    1. Use a Fallback for Print: If opening the PDF in a new tab, you might also consider implementing a fallback mechanism to handle printing, such as instructing the user to manually print the PDF if the automatic print dialog does not appear.

Each method has its trade-offs in terms of user experience and security, so choose the one that best aligns with your application’s needs and security policies.

webarp commented 6 months ago

Hi Stephan,

thank you very much for the clarifications.

What are you using, because if I leave the standard print button, I don't get any CSP error. Only the progress bar dialog is the problem.

I thought there is a way to open the printing dialog without opening first the progress bar dialog.

:)

webarp commented 6 months ago

Hi Stephan,

we added Content-Security-Policy: frame-src 'self' blob:; and it worked.

Thank you.

stephanrauh commented 6 months ago

That's the magic of AI: I've posted an answer that missed your question slightly, but even so, a short time later you come up with a solution... :)

I'm glad you've managed to solve the issue!