require 'net/http' require 'delegate' require 'stringio' class WickedPdf module WickedPdfHelper module Assets ASSET_URL_REGEX = /url\(['"]?([^'"]+?)['"]?\)/ class MissingAsset < StandardError; end class MissingLocalAsset < MissingAsset attr_reader :path def initialize(path) @path = path super("Could not find asset '#{path}'") end end class MissingRemoteAsset < MissingAsset attr_reader :url, :response def initialize(url, response) @url = url @response = response super("Could not fetch asset '#{url}': server responded with #{response.code} #{response.message}") end end class PropshaftAsset < SimpleDelegator def content_type super.to_s end def to_s content end def filename path.to_s end end class SprocketsEnvironment def self.instance @instance ||= Sprockets::Railtie.build_environment(Rails.application) end def self.find_asset(*args) instance.find_asset(*args) end end class LocalAsset attr_reader :path def initialize(path) @path = path end def content_type Mime::Type.lookup_by_extension(File.extname(path).delete('.')) end def to_s IO.read(path) end def filename path.to_s end end def wicked_pdf_asset_base64(path) asset = find_asset(path) raise MissingLocalAsset, path if asset.nil? base64 = Base64.encode64(asset.to_s).gsub(/\s+/, '') "data:#{asset.content_type};base64,#{Rack::Utils.escape(base64)}" end # Using `image_tag` with URLs when generating PDFs (specifically large PDFs with lots of pages) can cause buffer/stack overflows. # def wicked_pdf_url_base64(url) response = Net::HTTP.get_response(URI(url)) if response.is_a?(Net::HTTPSuccess) base64 = Base64.encode64(response.body).gsub(/\s+/, '') "data:#{response.content_type};base64,#{Rack::Utils.escape(base64)}" else Rails.logger.warn("[wicked_pdf] #{response.code} #{response.message}: #{url}") nil end end def wicked_pdf_stylesheet_link_tag(*sources) stylesheet_contents = sources.collect do |source| source = WickedPdfHelper.add_extension(source, 'css') "" end.join("\n") stylesheet_contents.gsub(ASSET_URL_REGEX) do if Regexp.last_match[1].starts_with?('data:') "url(#{Regexp.last_match[1]})" else "url(#{wicked_pdf_asset_path(Regexp.last_match[1])})" end end.html_safe end def wicked_pdf_stylesheet_pack_tag(*sources) return unless defined?(Webpacker) if running_in_development? stylesheet_pack_tag(*sources) else css_text = sources.collect do |source| source = WickedPdfHelper.add_extension(source, 'css') wicked_pdf_stylesheet_link_tag(webpacker_source_url(source)) end.join("\n") css_text.respond_to?(:html_safe) ? css_text.html_safe : css_text end end def wicked_pdf_javascript_pack_tag(*sources) return unless defined?(Webpacker) if running_in_development? javascript_pack_tag(*sources) else sources.collect do |source| source = WickedPdfHelper.add_extension(source, 'js') "" end.join("\n").html_safe end end def wicked_pdf_image_tag(img, options = {}) image_tag wicked_pdf_asset_path(img), options end def wicked_pdf_javascript_src_tag(jsfile, options = {}) jsfile = WickedPdfHelper.add_extension(jsfile, 'js') javascript_include_tag wicked_pdf_asset_path(jsfile), options end def wicked_pdf_javascript_include_tag(*sources) sources.collect do |source| source = WickedPdfHelper.add_extension(source, 'js') "" end.join("\n").html_safe end def wicked_pdf_asset_path(asset) if (pathname = asset_pathname(asset).to_s) =~ URI_REGEXP pathname else "file:///#{pathname}" end end def wicked_pdf_asset_pack_path(asset) return unless defined?(Webpacker) if running_in_development? asset_pack_path(asset) else wicked_pdf_asset_path webpacker_source_url(asset) end end private # borrowed from actionpack/lib/action_view/helpers/asset_url_helper.rb URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//} def asset_pathname(source) if precompiled_or_absolute_asset?(source) asset = asset_path(source) pathname = prepend_protocol(asset) if pathname =~ URI_REGEXP # asset_path returns an absolute URL using asset_host if asset_host is set pathname else File.join(Rails.public_path, asset.sub(/\A#{Rails.application.config.action_controller.relative_url_root}/, '')) end else asset = find_asset(source) if asset # older versions need pathname, Sprockets 4 supports only filename asset.respond_to?(:filename) ? asset.filename : asset.pathname else File.join(Rails.public_path, source) end end end def find_asset(path) if Rails.application.assets.respond_to?(:find_asset) Rails.application.assets.find_asset(path, :base_path => Rails.application.root.to_s) elsif defined?(Propshaft::Assembly) && Rails.application.assets.is_a?(Propshaft::Assembly) PropshaftAsset.new(Rails.application.assets.load_path.find(path)) elsif Rails.application.respond_to?(:assets_manifest) relative_asset_path = get_asset_path_from_manifest(path) return unless relative_asset_path asset_path = File.join(Rails.application.assets_manifest.dir, relative_asset_path) LocalAsset.new(asset_path) if File.file?(asset_path) else SprocketsEnvironment.find_asset(path, :base_path => Rails.application.root.to_s) end end def get_asset_path_from_manifest(path) assets = Rails.application.assets_manifest.assets if File.extname(path).empty? assets.find do |asset, _v| directory = File.dirname(asset) asset_path = File.basename(asset, File.extname(asset)) asset_path = File.join(directory, asset_path) if directory != '.' asset_path == path end&.last else assets[path] end end # will prepend a http or default_protocol to a protocol relative URL # or when no protcol is set. def prepend_protocol(source) protocol = WickedPdf.config[:default_protocol] || 'http' if source[0, 2] == '//' source = [protocol, ':', source].join elsif source[0] != '/' && !source[0, 8].include?('://') source = [protocol, '://', source].join end source end def precompiled_or_absolute_asset?(source) !Rails.configuration.respond_to?(:assets) || Rails.configuration.assets.compile == false || source.to_s[0] == '/' || source.to_s.match(/\Ahttps?\:\/\//) end def read_asset(source) asset = find_asset(source) return asset.to_s.force_encoding('UTF-8') if asset unless precompiled_or_absolute_asset?(source) raise MissingLocalAsset, source if WickedPdf.config[:raise_on_missing_assets] return end pathname = asset_pathname(source) if pathname =~ URI_REGEXP read_from_uri(pathname) elsif File.file?(pathname) IO.read(pathname) elsif WickedPdf.config[:raise_on_missing_assets] raise MissingLocalAsset, pathname if WickedPdf.config[:raise_on_missing_assets] end end def read_from_uri(uri) response = Net::HTTP.get_response(URI(uri)) unless response.is_a?(Net::HTTPSuccess) raise MissingRemoteAsset.new(uri, response) if WickedPdf.config[:raise_on_missing_assets] return end asset = response.body asset.force_encoding('UTF-8') if asset asset = gzip(asset) if WickedPdf.config[:expect_gzipped_remote_assets] asset end def gzip(asset) stringified_asset = StringIO.new(asset) gzipper = Zlib::GzipReader.new(stringified_asset) gzipper.read rescue Zlib::GzipFile::Error nil end def webpacker_source_url(source) return unless webpacker_version # In Webpacker 3.2.0 asset_pack_url is introduced if webpacker_version >= '3.2.0' if (host = Rails.application.config.asset_host) asset_pack_path(source, :host => host) else asset_pack_url(source) end else source_path = asset_pack_path(source) # Remove last slash from root path root_url[0...-1] + source_path end end def running_in_development? return unless webpacker_version # :dev_server method was added in webpacker 3.0.0 if Webpacker.respond_to?(:dev_server) Webpacker.dev_server.running? else Rails.env.development? || Rails.env.test? end end def webpacker_version if defined?(Shakapacker) require 'shakapacker/version' Shakapacker::VERSION elsif defined?(Webpacker) require 'webpacker/version' Webpacker::VERSION end end end end end