bidi2pdf-rails 0.0.1.pre.alpha → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.idea/bidi2pdf-rails.iml +55 -9
  3. data/.rubocop.yml +14 -0
  4. data/CHANGELOG.md +33 -0
  5. data/README.md +117 -27
  6. data/Rakefile +2 -0
  7. data/cliff.toml +126 -0
  8. data/lib/bidi2pdf_rails/browser_console_log_subscriber.rb +24 -0
  9. data/lib/bidi2pdf_rails/chromedriver_manager_singleton.rb +11 -11
  10. data/lib/bidi2pdf_rails/config.rb +133 -0
  11. data/lib/bidi2pdf_rails/configurable.rb +106 -0
  12. data/lib/bidi2pdf_rails/main_log_subscriber.rb +33 -0
  13. data/lib/bidi2pdf_rails/network_log_subscriber.rb +20 -0
  14. data/lib/bidi2pdf_rails/railtie.rb +12 -45
  15. data/lib/bidi2pdf_rails/services/html_renderer.rb +33 -0
  16. data/lib/bidi2pdf_rails/services/html_to_pdf_converter.rb +28 -0
  17. data/lib/bidi2pdf_rails/services/pdf_browser_session.rb +39 -0
  18. data/lib/bidi2pdf_rails/services/pdf_renderer.rb +82 -0
  19. data/lib/bidi2pdf_rails/services/url_to_pdf_converter.rb +82 -0
  20. data/lib/bidi2pdf_rails/version.rb +1 -1
  21. data/lib/bidi2pdf_rails.rb +41 -58
  22. data/lib/generators/bidi2pdf_rails/USAGE +12 -4
  23. data/lib/generators/bidi2pdf_rails/initializer_generator.rb +136 -30
  24. data/lib/generators/bidi2pdf_rails/templates/bidi2pdf_rails.rb.tt +25 -79
  25. data/spec/acceptance/user_can_download_report_pdf_spec.rb +133 -0
  26. data/spec/acceptance/user_can_generate_pdf_from_protected_remote_url_spec.rb +173 -0
  27. data/spec/dummy/app/controllers/reports_controller.rb +37 -0
  28. data/spec/dummy/app/controllers/secure_controller.rb +52 -0
  29. data/spec/dummy/app/views/layouts/simple.html.erb +17 -0
  30. data/spec/dummy/app/views/secure/show.html.erb +10 -0
  31. data/spec/dummy/config/environments/production.rb +1 -1
  32. data/spec/dummy/config/initializers/bidi2pdf_rails.rb +68 -54
  33. data/spec/dummy/config/initializers/cors.rb +1 -1
  34. data/spec/dummy/config/routes.rb +10 -0
  35. data/spec/dummy/log/development.log +16567 -156
  36. data/spec/dummy/log/test.log +53046 -0
  37. data/spec/dummy/tmp/pids/server.pid +1 -1
  38. data/spec/integration/generators/bidi2pdf_rails/initializer_generator_spec.rb +64 -0
  39. data/spec/rails_helper.rb +8 -1
  40. data/spec/spec_helper.rb +47 -5
  41. data/spec/support/default_dirs_helper.rb +32 -0
  42. data/spec/support/pdf_helper.rb +12 -0
  43. data/spec/support/render_setting_helpers.rb +28 -0
  44. data/spec/support/request_server_bootstrap.rb +44 -0
  45. data/spec/{bidi2pdf_rails → unit/bidi2pdf_rails}/bidi2pdf_rails_spec.rb +1 -1
  46. data/spec/unit/bidi2pdf_rails/configurable/base_nested_config_spec.rb +133 -0
  47. data/tasks/changelog.rake +29 -0
  48. data/tasks/coverage.rake +23 -0
  49. metadata +95 -25
  50. data/lib/bidi2pdf_rails/log_subscriber.rb +0 -13
  51. data/spec/dummy/spec/helpers/reports_helper_spec.rb +0 -15
  52. data/spec/dummy/spec/requests/reports_spec.rb +0 -10
  53. data/spec/dummy/spec/views/reports/show.html.erb_spec.rb +0 -5
  54. data/spec/generator/bidie2pdf_rails_initializer_generator_spec.rb +0 -5
  55. data/spec/generator/initializer_generator_spec.rb +0 -5
  56. data/spec/requests/reports_spec.rb +0 -17
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+ require "net/http"
5
+ require "rack/handler/puma"
6
+ require "socket"
7
+ require "base64"
8
+
9
+ RSpec.feature "As a user, I want to generate a PDF from a protected remote URL", :pdf, type: :request do
10
+ before(:all) do
11
+ # Bidi2pdfRails.config.general_options.headless = false
12
+ Bidi2pdfRails::ChromedriverManagerSingleton.initialize_manager force: true
13
+ end
14
+
15
+ after(:all) do
16
+ Bidi2pdfRails::ChromedriverManagerSingleton.shutdown
17
+ end
18
+
19
+ scenario "Using basic auth for remote PDF rendering" do
20
+ # Controller setup:
21
+ #
22
+ # You can configure basic auth in two ways:
23
+ #
24
+ # 1. In an initializer (global config):
25
+ #
26
+ # Bidi2pdfRails.configure do |config|
27
+ # config.render_remote_settings.basic_auth_user = ->(_controller) { "admin" }
28
+ # config.render_remote_settings.basic_auth_pass = ->(_controller) { "secret" }
29
+ # end
30
+ #
31
+ # 2. Inline within controller action:
32
+ #
33
+ # render pdf: 'convert-remote-url-basic-auth',
34
+ # url: basic_auth_endpoint_url(only_path: false),
35
+ # auth: { username: "admin", password: "secret" }
36
+
37
+ when_ "I request a PDF generated from a basic-auth protected page" do
38
+ before do
39
+ with_render_setting :basic_auth_user, ->(_controller) { "admin" }
40
+ # in prod better to use:
41
+ # Bidi2pdfRails.config.render_remote_settings.basic_auth_pass = ->(_controller) { Rails.application.credentials.dig('bidi2pdf_rails', 'basic_auth_pass') }
42
+ with_render_setting :basic_auth_pass, ->(_controller) { "secret" }
43
+
44
+ @response = get_pdf_response "/convert-remote-url-basic-auth"
45
+ end
46
+
47
+ then_ "I receive a successful HTTP response" do
48
+ expect(@response.code).to eq("200")
49
+ end
50
+
51
+ and_ "I receive a PDF file in response" do
52
+ expect(@response['Content-Type']).to eq("application/pdf")
53
+ end
54
+
55
+ and_ "the PDF contains the expected number of pages" do
56
+ expect(@response.body).to have_pdf_page_count(1)
57
+ end
58
+
59
+ and_ "the disposition header is set to attachment" do
60
+ expect(@response['Content-Disposition']).to start_with('inline; filename="convert-remote-url-basic-auth.pdf"')
61
+ end
62
+
63
+ and_ "the PDF contains the expected content" do
64
+ expect(@response.body).to contains_pdf_text("This page is secured with: HTTPBasicAuthentication").at_page(1)
65
+ end
66
+ end
67
+ end
68
+
69
+ scenario "Using a session cookie for remote PDF rendering" do
70
+ # Controller setup:
71
+ #
72
+ # You can configure cookies in two ways:
73
+ #
74
+ # 1. In an initializer (global config):
75
+ #
76
+ # Bidi2pdfRails.configure do |config|
77
+ # config.render_remote_settings.cookies = = ->(controller) { { "auth_token" => signed_cookie_value("valid-authentication-token", controller) } }
78
+ # end
79
+ #
80
+ # 2. Inline within controller action:
81
+ #
82
+ # render pdf: 'convert-remote-url-cookie',
83
+ # url: cookie_endpoint_url(only_path: false),
84
+ # wait_for_page_loaded: false,
85
+ # cookies: { "auth_token" => signed_cookie_value("valid-authentication-token", controller) }
86
+ #
87
+
88
+ when_ "I request a PDF from a page that requires a session cookie" do
89
+ def signed_cookie_value(value, controller)
90
+ request = controller.request
91
+ secret = request.key_generator.generate_key(request.signed_cookie_salt)
92
+ # Sign the value
93
+ verifier = ActiveSupport::MessageVerifier.new(secret)
94
+ verifier.generate(value)
95
+ end
96
+
97
+ before do
98
+ with_render_setting :cookies, ->(controller) { { "auth_token" => signed_cookie_value("valid-authentication-token", controller) } }
99
+
100
+ @response = get_pdf_response "/convert-remote-url-cookie"
101
+ end
102
+
103
+ then_ "I receive a successful HTTP response" do
104
+ expect(@response.code).to eq("200")
105
+ end
106
+
107
+ and_ "I receive a PDF file in response" do
108
+ expect(@response['Content-Type']).to eq("application/pdf")
109
+ end
110
+
111
+ and_ "the PDF contains the expected number of pages" do
112
+ expect(@response.body).to have_pdf_page_count(1)
113
+ end
114
+
115
+ and_ "the disposition header is set to attachment" do
116
+ expect(@response['Content-Disposition']).to start_with('inline; filename="convert-remote-url-cookie.pdf"')
117
+ end
118
+
119
+ and_ "the PDF contains the expected content" do
120
+ expect(@response.body).to contains_pdf_text("Protected Resource This page is secured with: CookieAuthentication").at_page(1)
121
+ end
122
+ end
123
+ end
124
+
125
+ scenario "Using custom headers for remote PDF rendering" do
126
+ # Controller setup:
127
+ #
128
+ # You can configure headers in two ways:
129
+ #
130
+ # 1. In an initializer (global config):
131
+ #
132
+ # Bidi2pdfRails.configure do |config|
133
+ # config.render_remote_settings.headers = ->(controller) { { "X-API-Key" => "your-secret-api-key" } }
134
+ # end
135
+ #
136
+ # 2. Inline within controller action:
137
+ #
138
+ # render pdf: 'convert-remote-url-cookie',
139
+ # url: api_endpoint_url(only_path: false),
140
+ # wait_for_page_loaded: false,
141
+ # headers: { "X-API-Key" => "your-secret-api-key" }
142
+ #
143
+
144
+ when_ "I request a PDF from a page that requires an auth header" do
145
+ before do
146
+ @old_headers = Bidi2pdfRails.config.render_remote_settings.headers
147
+ with_render_setting :headers, ->(_controller) { { "X-API-Key" => "your-secret-api-key" } }
148
+
149
+ @response = get_pdf_response "/convert-remote-url-header"
150
+ end
151
+
152
+ then_ "I receive a successful HTTP response" do
153
+ expect(@response.code).to eq("200")
154
+ end
155
+
156
+ and_ "I receive a PDF file in response" do
157
+ expect(@response['Content-Type']).to eq("application/pdf")
158
+ end
159
+
160
+ and_ "the PDF contains the expected number of pages" do
161
+ expect(@response.body).to have_pdf_page_count(1)
162
+ end
163
+
164
+ and_ "the disposition header is set to attachment" do
165
+ expect(@response['Content-Disposition']).to start_with('inline; filename="convert-remote-url-cookie.pdf"')
166
+ end
167
+
168
+ and_ "the PDF contains the expected content" do
169
+ expect(@response.body).to contains_pdf_text("Protected Resource This page is secured with: API KeyAuthentication").at_page(1)
170
+ end
171
+ end
172
+ end
173
+ end
@@ -5,4 +5,41 @@ class ReportsController < ApplicationController
5
5
  format.pdf { render pdf: 'my-report', layout: 'pdf' }
6
6
  end
7
7
  end
8
+
9
+ def convert_remote_url
10
+ render pdf: 'convert-remote-url', url: "http://example.com", wait_for_page_loaded: false, print_options: { page: { format: :A4 } }
11
+ end
12
+
13
+ def inline_html
14
+ html = <<~HTML
15
+ <html>
16
+ <head>
17
+ </head>
18
+ <body>
19
+ <h1>PDF Rendering Sample</h1>
20
+ <p style="page-break-after: always;">Page break</p>
21
+ <p>Content Page 2</p>
22
+ </body>
23
+ </html>
24
+ HTML
25
+ render pdf: 'inline-html', inline: html, wait_for_page_loaded: false, print_options: { page: { format: :A4 } }
26
+ end
27
+
28
+ def convert_remote_url_basic_auth
29
+ render pdf: 'convert-remote-url-basic-auth',
30
+ url: basic_auth_endpoint_url(only_path: false),
31
+ wait_for_page_loaded: false
32
+ end
33
+
34
+ def convert_remote_url_cookie
35
+ render pdf: 'convert-remote-url-cookie',
36
+ url: cookie_endpoint_url(only_path: false),
37
+ wait_for_page_loaded: false
38
+ end
39
+
40
+ def convert_remote_url_header
41
+ render pdf: 'convert-remote-url-cookie',
42
+ url: api_endpoint_url(only_path: false),
43
+ wait_for_page_loaded: false
44
+ end
8
45
  end
@@ -0,0 +1,52 @@
1
+ # app/controllers/secure_controller.rb
2
+ class SecureController < ApplicationController
3
+ # Basic Auth protection for the first action
4
+ http_basic_authenticate_with name: "admin", password: "secret", only: :basic_auth_endpoint
5
+
6
+ before_action :authenticate_with_api_key, only: :api_endpoint
7
+ before_action :authenticate_with_cookie, only: :cookie_endpoint
8
+
9
+ def basic_auth_endpoint
10
+ @auth_method = "HTTP Basic Authentication"
11
+ @auth_description = "This endpoint requires username and password authentication."
12
+ @auth_details = "Credentials: username: <code>admin</code>, password: <code>secret</code>"
13
+
14
+ render :show, layout: 'simple'
15
+ end
16
+
17
+ def api_endpoint
18
+ @auth_method = "API Key Authentication"
19
+ @auth_description = "This endpoint requires an API key in the header."
20
+ @auth_details = "Required header: <code>X-API-Key: your-secret-api-key</code>"
21
+
22
+ render :show, layout: 'simple'
23
+ end
24
+
25
+ def cookie_endpoint
26
+ @auth_method = "Cookie Authentication"
27
+ @auth_description = "This endpoint requires an authentication cookie."
28
+ @auth_details = "Required cookie: <code>auth_token</code> with value <code>valid-authentication-token</code>"
29
+
30
+ render :show, layout: 'simple'
31
+ end
32
+
33
+ private
34
+
35
+ def authenticate_with_api_key
36
+ api_key = request.headers["X-API-Key"]
37
+ valid_key = "your-secret-api-key"
38
+
39
+ if api_key.blank? || api_key != valid_key
40
+ render html: "<h1>Unauthorized</h1><p>Invalid or missing API key</p>".html_safe, status: :unauthorized
41
+ end
42
+ end
43
+
44
+ def authenticate_with_cookie
45
+ auth_token = cookies.signed[:auth_token]
46
+ valid_token = "valid-authentication-token"
47
+
48
+ if auth_token.blank? || auth_token != valid_token
49
+ render html: "<h1>Unauthorized</h1><p>Invalid or missing API key</p>".html_safe, status: :unauthorized
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= content_for(:title) || "Dummy" %></title>
5
+ <%= csp_meta_tag %>
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <meta name="apple-mobile-web-app-capable" content="yes">
8
+ <%= csrf_meta_tags %>
9
+ <%= csp_meta_tag %>
10
+
11
+ <%= yield :head %>
12
+ </head>
13
+
14
+ <body>
15
+ <%= yield %>
16
+ </body>
17
+ </html>
@@ -0,0 +1,10 @@
1
+ # app/views/secure/show.html.erb
2
+ <h1>Protected Resource</h1>
3
+ <p>This page is secured with: <strong><%= @auth_method %></strong></p>
4
+ <p><%= @auth_description %></p>
5
+
6
+ <div class="auth-details">
7
+ <%= @auth_details %>
8
+ </div>
9
+
10
+ <p>When authenticated, you can access this content.</p>
@@ -46,7 +46,7 @@ Rails.application.configure do
46
46
  .then { |logger| ActiveSupport::TaggedLogging.new(logger) }
47
47
 
48
48
  # Prepend all log lines with the following tags.
49
- config.log_tags = [ :request_id ]
49
+ config.log_tags = [:request_id]
50
50
 
51
51
  # "info" includes generic and useful information about system operation, but avoids logging too much
52
52
  # information to avoid inadvertent exposure of personally identifiable information (PII). If you
@@ -3,62 +3,76 @@
3
3
  Bidi2pdfRails.configure do |config|
4
4
  overrides = Rails.application.config.x.bidi2pdf_rails
5
5
 
6
- config.notification_service = ActiveSupport::Notifications
7
-
8
- config.logger = Rails.logger
9
- config.verbosity = overrides.verbosity.nil? ? :none : overrides.verbosity
10
- config.default_timeout = 10
11
-
12
- # Logging options
13
- config.log_network_events = false
14
- config.log_browser_console = false
15
-
16
- # Chrome & BiDi
17
- # config.remote_browser_url = nil
18
- config.headless = overrides.headless.nil? ? false : overrides.headless
19
- # config.chromedriver_port = 0
20
- # config.chrome_session_args = [
21
- # "--disable-gpu",
22
- # "--no-sandbox"
23
- # ]
24
-
25
-
26
- # config.proxy_addr = nil
27
- # config.proxy_port = nil
28
- # config.proxy_user = nil
29
- # config.proxy_pass = nil
30
-
31
-
32
-
33
- # Viewport settings
34
- # config.viewport_width = 1920
35
- # config.viewport_height = 1080
36
-
37
-
38
-
39
- # PDF settings
40
- # config.pdf_orientation = "portrait"
41
- # config.pdf_margin_top = 10
42
- # config.pdf_margin_bottom = 10
43
- # config.pdf_margin_left = 10
44
- # config.pdf_margin_right = 10
45
- # config.pdf_print_background = true
46
- # config.pdf_scale = 1.0
47
-
6
+ #
7
+ # General Options
8
+ #
9
+
10
+ # config.general_options.logger = Rails.logger # The logger
11
+
12
+ # Allowed values: "none", "low", "medium", "high"
13
+ config.general_options.verbosity = "medium" # How verbose to be
14
+ # config.general_options.headless = !Rails.env.development? # Run Chrome in headless mode
15
+ # config.general_options.wait_for_network_idle = true # Wait for network idle
16
+ config.general_options.wait_for_page_loaded = true # Wait for page loaded
17
+ # config.general_options.wait_for_page_check_script = nil # Wait for page check script
18
+ # config.general_options.notification_service = -> { ActiveSupport::Notifications } # Notification service
19
+ # config.general_options.default_timeout = 10 # Default timeout for various Bidi commands
20
+ # config.general_options.chrome_session_args = ["--disable-gpu", "--disable-popup-blocking", "--disable-hang-monitor"] # Chrome session arguments
21
+
22
+ #
23
+ # Chromedriver Settings (when chromedriver run within your app)
24
+ #
25
+
26
+ # config.chromedriver_settings.install_dir = nil # Chromedriver install directory
27
+ # config.chromedriver_settings.port = 0 # Chromedriver port
28
+
29
+ #
30
+ # Proxy Settings
31
+ #
32
+
33
+ # config.proxy_settings.addr = nil # Proxy address (e.g., 127.0.0.1)
34
+ # config.proxy_settings.port = nil # Proxy port (e.g., 8080)
35
+ # config.proxy_settings.user = nil # Proxy user
36
+ # config.proxy_settings.pass = -> { Rails.application.credentials.dig('bidi2pdf_rails', 'proxy_pass') } # Proxy password
37
+
38
+ #
39
+ # PDF Settings
40
+ #
41
+
42
+ # Allowed values: "portrait", "landscape"
43
+ # config.pdf_settings.orientation = "portrait" # PDF orientation (portrait/landscape)
44
+ # config.pdf_settings.margins = false # Configure PDF margins?
45
+ # config.pdf_settings.margin_top = 2.5 # PDF margin top (cm)
46
+ # config.pdf_settings.margin_bottom = 2 # PDF margin bottom (cm)
47
+ # config.pdf_settings.margin_left = 2 # PDF margin left (cm)
48
+ # config.pdf_settings.margin_right = 2 # PDF margin right (cm)
49
+
50
+ # Allowed values: "letter", "legal", "tabloid", "ledger", "a0", "a1", "a2", "a3", "a4", "a5", "a6"
51
+ # config.pdf_settings.page_format = nil # PDF page format (e.g., A4)
52
+ # config.pdf_settings.page_width = 21.0 # PDF page width (cm, not needed when format is specified)
53
+ # config.pdf_settings.page_height = 29.7 # PDF page height (cm, not needed when format is specified)
54
+ # config.pdf_settings.print_background = true # Print background graphics?
55
+ # config.pdf_settings.scale = 1.0 # PDF scale (e.g., 1.0)
56
+ # config.pdf_settings.shrink_to_fit = false # Shrink to fit?
57
+
58
+ #
59
+ # Remote URL Settings
60
+ #
61
+
62
+ # config.render_remote_settings.browser_url = nil # Remote browser URL (e.g. http://localhost:3001/sesion)
63
+ # config.render_remote_settings.basic_auth_user = nil # Basic auth user
64
+ # config.render_remote_settings.basic_auth_pass = -> { Rails.application.credentials.dig('bidi2pdf_rails', 'basic_auth_pass') } # Basic auth password
65
+ # config.render_remote_settings.headers = {"X-API-INFO" => "my info"} # Headers to be send when allong an url
66
+ # config.render_remote_settings.cookies = {"session_id" => "my session"} # Cookies to be send when alling an url
67
+ end
48
68
 
49
- # config.cookies = [
50
- # { name: "session", value: "abc123", domain: "example.com" }
51
- # ]
69
+ Rails.application.config.after_initialize do
70
+ Bidi2pdfRails::MainLogSubscriber.attach_to "bidi2pdf", inherit_all: true # needed for imported methods
71
+ Bidi2pdfRails::MainLogSubscriber.attach_to "bidi2pdf_rails", inherit_all: true # needed for imported methods
52
72
 
53
- # config.headers = {
54
- # "X-API-KEY" => "topsecret"
55
- # }
73
+ Bidi2pdfRails::BrowserConsoleLogSubscriber.attach_to "bidi2pdf"
56
74
 
57
- # config.auth = {
58
- # username: "admin",
59
- # password: "secret"
60
- # }
75
+ Bidi2pdfRails::MainLogSubscriber.silence /network_event_.*\.bidi2pdf/
61
76
 
62
- # chromedriver install dir
63
- # config.install_dir = Rails.root.join("tmp", "bidi2pdf").to_s
77
+ Bidi2pdfRails::NetworkLogSubscriber.attach_to "bidi2pdf"
64
78
  end
@@ -4,6 +4,6 @@ gem 'rack-cors'
4
4
  Rails.application.config.middleware.insert_before 0, Rack::Cors do
5
5
  allow do
6
6
  origins '*'
7
- resource '/assets/*', headers: :any, methods: [ :get, :options ]
7
+ resource '/assets/*', headers: :any, methods: [:get, :options]
8
8
  end
9
9
  end
@@ -1,6 +1,16 @@
1
1
  Rails.application.routes.draw do
2
2
  get "reports/:id" => "reports#show", as: :report
3
3
 
4
+ get "convert-remote-url" => "reports#convert_remote_url", as: :print_remote
5
+ get "inline-html" => "reports#inline_html", as: :print_inline
6
+ get "convert-remote-url-basic-auth" => "reports#convert_remote_url_basic_auth", as: :print_remote_basic_auth
7
+ get "convert-remote-url-cookie" => "reports#convert_remote_url_cookie", as: :print_remote_url_cookie
8
+ get "convert-remote-url-header" => "reports#convert_remote_url_header", as: :print_remote_url_header
9
+
10
+ get 'basic-auth', to: 'secure#basic_auth_endpoint', as: :basic_auth_endpoint
11
+ get 'header-auth', to: 'secure#api_endpoint', as: :api_endpoint
12
+ get 'cookie-auth', to: 'secure#cookie_endpoint', as: :cookie_endpoint
13
+
4
14
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
5
15
 
6
16
  # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.