mileszs / wicked_pdf

PDF generator (from HTML) plugin for Ruby on Rails
http://www.mileszs.com/wicked-pdf-plugin
MIT License
3.54k stars 647 forks source link

Ajax not executed in JavaScript whilst converting to PDF #880

Open PedroBorder opened 4 years ago

PedroBorder commented 4 years ago

## Issue description

We are attempting to convert to PDF a view that uses Javascript with an AJAX call to set styles, classes to the html. This is working perfectly but when converting to PDF these "styles" set inside the AJAX are not shown.

## Expected or desired behavior

We want to verify if the issue is the AJAX or the Javascript, how could we verify this? We've investigated about this and the Javascript shouldnt be an issue as we've checked the creation of these files using wicked_pdf_javascript_include_tag and they are being included.

## System specifications

wicked_pdf gem version: 0.11.0

wkhtmltopdf version: wkhtmltopdf 0.12.1 (with patched qt)

platform/distribution and version: Centos 6

unixmonkey commented 4 years ago

It's pretty likely you are actually running into a cross-domain issue, since you are fetching ajax from a temporary .html file on-disk, it may not be allowed from your server.

Check this out, it may help.

Also, you might try setting up WickedPDF to act as a middleware only on the page you are doing this for.

Let me know what you find, and how it goes!

PedroBorder commented 4 years ago

Thanks for your reply @unixmonkey, here our updates:

We've replaced the AJAX call with XMLHttpRequest and it is all looking good except when it "reaches" the controller it doesnt execute the method giving an error 401 and the XMLHttpRequest.status() is returning 0 instead of 200.

The call method after initializing everything: (this is working in the normal HTML but giving the error when running the wicked gem)

    httpRequest.open('GET', url, true);
    httpRequest.withCredentials = true;
    httpRequest.setRequestHeader('Accept', 'application/json, text/javascript');

    httpRequest.onreadystatechange = function(){
      $container.find('h4').html(JSON.stringify("ERROR"));
      if (httpRequest.readyState === 4 && httpRequest.status === 200) {
        $container.find('h4').html(JSON.stringify("SUCCESS"));
      }
    };

    httpRequest.send();

This is the output in the logs, as shown below it looks like he knows where and what to access but then it is giving the 401 Unauthorized message.

INFO -- : Started GET "/monitor/devicestatus_alarms/29" for ::1 at 2020-01-23 15:19:18 +0000
INFO -- : Processing by MonitorController#devicestatus_alarms as JSON
INFO -- :   Parameters: {"id"=>"29"}
INFO -- : Completed 401 Unauthorized in 6ms (ActiveRecord: 0.0ms)

We have been messing around with the CORS but are not able to add the Access-Control-Allow-Credentials to the Response Headers.

Thanks in advance for your time, hope to hear back from you soon!

unixmonkey commented 4 years ago

401 unauthorized generally means you either aren't logged in or don't have the access level needed to request the resource. When using HTML or debug mode, your browser already has access to the session, so you are logged in when you make the request. When wkhtmltopdf is executed, it's as if you are a logged-out user.

It sounds like you need to either pass along an Authorization header or cookie.

Check out the README on how to pass along cookies or basic auth, or try using wicked_pdf as a middleware. When it does that it loads the page as-is, but changes relative URLs into fully-qualified ones before rendering. Not sure if it'll work here, but not too hard to try out.

PedroBorder commented 4 years ago

Thanks for your quick reply @unixmonkey

We are adding a cookie now like this following the readme syntax (we have tried all possible ways we could think of:

    respond_to do |format|
      format.pdf do
        render pdf: "#{@report.name}",
               page_size: 'A4',
               javascript_delay: 3000,
               cookie: '_session_id 8cf450ac2bb3893db315313f3995354f',
               orientation: @report.orientation_name,
               margin: { top: 5, bottom: 5 },
               show_as_html: params[:debug]
      end
    end

And we are getting this error:

RuntimeError - Error: Failed to execute:
["/usr/local/bin/wkhtmltopdf", "-q", "--margin-top", "5", "--margin-bottom", "5", "--orientation", "Landscape", "--page-size", "A4", "--cookie", "_session_id 8cf450ac2bb3893db315313f3995354f", "--javascript-delay", "3000", "file:///tmp/wicked_pdf20200123-9086-hn1qbs.html", "/tmp/wicked_pdf_generated_file20200123-9086-g9s67h.pdf"]
Error: PDF could not be generated!

When removing the line with "cookie" there is no error. We've also tried with the headers Credentials but no luck because we think we are doing it wrong but haven't found a "proper" guide for this

Thanks in advance!

unixmonkey commented 4 years ago

Ah, seems you have stumbled across an old bug.

Notice how in your command it comes out like this:

"--cookie", "_session_id 8cf450ac2bb3893db315313f3995354f"

Where the cookie key and values are both treated as a key only, and are missing the value.

This should be coming out like this instead (and does for me on the current version of wicked_pdf):

"--cookie", "_session_id" "8cf450ac2bb3893db315313f3995354f"

When I dropped back down to wicked_pdf 0.11.0 like you said you are using, I can replicate your issue, so I can confidently say this is a fixed issue in the current version.

I would encourage you to try and upgrade wicked_pdf if you can; it should not have any breaking changes, but it may not work on very old Ruby versions.

I might not upgrade wkhtmltopdf though, until you've visually verified all your PDFs still look the same (it can sometimes have subtle changes like zoom level, fonts, issues with HTTPS, etc).

If you cannot or do not wish to upgrade right now, you can "work around" the bad command by injecting it directly with the extra property like this:

render pdf: "#{@report.name}",
  extra: '--cookie _session_id 8cf450ac2bb3893db315313f3995354f'

Let me know what you end up doing, and how it goes!

PedroBorder commented 4 years ago

Thanks again for the elaborated response @unixmonkey

We've added the extra: '--cookie _session_id... parameter in the render pdf part and it is now correctly entering the controller. The issue is that the status is still returning 0 when we run it from the PDF part. If we just run same process from the html it is working correctly returning a status of 200.

Are we missing something else?

    httpRequest.open('GET', url);
    httpRequest.withCredentials = true;
    httpRequest.responseType = 'json';
    httpRequest.setRequestHeader('Accept', 'application/json');

    httpRequest.onreadystatechange = function(){
      $container.find('h4').html(JSON.stringify(httpRequest.response));

      if (httpRequest.readyState === 4 && httpRequest.status === 200) {
        $container.find('h4').html(JSON.stringify(httpRequest.response));
      }
    };

    httpRequest.send();

Controller is returning it like this:

render :json => @status, :layout => false

Logs:

INFO -- : Started GET "/monitor/devicestatus_alarms/30" for ::1 at 2020-01-24 09:22:18 +0000
INFO -- : Processing by MonitorController#devicestatus_alarms as JSON
INFO -- :   Parameters: {"id"=>"30"}

INFO -- : Completed 200 OK in 45ms (Views: 0.2ms | ActiveRecord: 18.3ms)
unixmonkey commented 4 years ago

If session and cross-domain stuff is sorted as it sounds like it is, there's a few things you can try:

Try using the javascript_delay or window_status options to inform wkhtmltopdf to wait until your page is fully rendered (JS, Ajax, and all) before taking it's "PDF screenshot".

For instance, I think

render pdf: "mypdf", javascript_delay: 5000

Should give it 5 seconds to fully render.

If that isn't enough (or is too variable to pick a time), you can use window_status like this:

render pdf: "mypdf", window_status: 'done_with_ajax'
httpRequest.onreadystatechange = function(){
      $container.find('h4').html(JSON.stringify(httpRequest.response));

      if (httpRequest.readyState === 4 && httpRequest.status === 200) {
        $container.find('h4').html(JSON.stringify(httpRequest.response));
        window.status = 'done_with_ajax'; // TELL WKHTMLTOPDF WE ARE DONE
      }
    };

If those don't work, it may be an issue with the web rendering engine wkhtmltopdf uses under the hood. It's basically equivalent to Chrome 13 (which is several years old now). If you are using fancy ES6 functions without transpiling, they may be failing. It may be worth trying to upgrade wkhtmltopdf at this point, but even then I wouldn't count on it fixing it.

Good luck! Let me know how it goes.

PedroBorder commented 4 years ago

Good morning @unixmonkey

I'm sorry for the delayed response but got dragged to another project. Now I'm back at it and still the issue remains, I went over all the things you told us and tested it once again but it isnt working...

When I set the option show_as_html: true instead of show_as_html: params[:debug] it's working and showing up correctly. But this is because its loading directly as an HTML which is autonomously loading the JS I think.

Besides using a javascript_delay or a window_status can you think of any other posibilities?

Also, when doing the show_as_html: true I can add console.log to the JS and more or less do some debugging, but this doesnt work when setting it as show_as_html: params[:debug] so I dont really know what is going on and what is loading

unixmonkey commented 4 years ago

console.log won't do anything for you in a PDF, but you could use document.write('hello') or document.getElementById('some_id').appendChild(document.createTextNode("Hello World")) to verify visually if you JS is working, but I still suspect you have a cross-domain issue.

Why don't you try configuring the middleware to only work on that route and see if it does any better?

veeKewl commented 4 years ago

console.log won't do anything for you in a PDF, but you could use document.write('hello') or document.getElementById('some_id').appendChild(document.createTextNode("Hello World")) to verify visually if you JS is working, but I still suspect you have a cross-domain issue.

Why don't you try configuring the middleware to only work on that route and see if it does any better?

Hiiii !! :) i think im having a similar issue, but it's not AJAX it's my javascript, the script in my URL works perfectly fine in my browser, but if i try to generate it using pdf_from_url any script that came from my included js does not work, yeah i also confirmed that it got included, it's just not working. I have filed an issue here. https://github.com/mileszs/wicked_pdf/issues/906 . I tried put just a document.getElementsByClassName('document')[0].style.display = "none"; in the script to see if it works but nah.