diff --git a/app/services/images/optimizer.rb b/app/services/images/optimizer.rb index 7b3ca488e..f1262a656 100644 --- a/app/services/images/optimizer.rb +++ b/app/services/images/optimizer.rb @@ -7,6 +7,8 @@ module Images imgproxy(img_src, **kwargs) elsif cloudinary_enabled? cloudinary(img_src, **kwargs) + elsif cloudflare_enabled? + cloudflare(img_src, **kwargs) else img_src end @@ -23,6 +25,21 @@ module Images sign_url: true }.freeze + def self.cloudflare(img_src, **kwargs) + template = Addressable::Template.new("https://{domain}/cdn-cgi/image/{options*}/{src}") + template.expand( + domain: ApplicationConfig["CLOUDFLARE_IMAGES_DOMAIN"], + options: { + width: kwargs[:width], + height: kwargs[:height], + fit: "cover", + gravity: "auto", + format: "auto" + }, + src: extract_suffix_url(img_src), + ).to_s + end + def self.cloudinary(img_src, **kwargs) options = DEFAULT_CL_OPTIONS.merge(kwargs).compact_blank @@ -68,6 +85,10 @@ module Images config.cloud_name.present? && config.api_key.present? && config.api_secret.present? end + def self.cloudflare_enabled? + ApplicationConfig["CLOUDFLARE_IMAGES_DOMAIN"].present? + end + def self.get_imgproxy_endpoint if Rails.env.production? # Use /images with the same domain on Production as @@ -81,5 +102,14 @@ module Images ApplicationConfig["IMGPROXY_ENDPOINT"] || "http://localhost:8080" end end + + def self.extract_suffix_url(full_url) + prefix = "https://#{ApplicationConfig['CLOUDFLARE_IMAGES_DOMAIN']}/cdn-cgi/image" + return full_url unless full_url&.starts_with?(prefix) + + uri = URI.parse(full_url) + match = uri.path.match(%r{https?.+}) + CGI.unescape(match[0]) if match + end end end diff --git a/spec/services/images/optimizer_spec.rb b/spec/services/images/optimizer_spec.rb index 05836212e..721a89fca 100644 --- a/spec/services/images/optimizer_spec.rb +++ b/spec/services/images/optimizer_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Images::Optimizer, type: :service do describe "#call" do before do allow(described_class).to receive(:cloudinary) + allow(described_class).to receive(:cloudflare) allow(described_class).to receive(:imgproxy) end @@ -85,6 +86,52 @@ RSpec.describe Images::Optimizer, type: :service do end end + describe "#cloudflare" do + before do + allow(ApplicationConfig).to receive(:[]).with("CLOUDFLARE_IMAGES_DOMAIN").and_return("images.example.com") + end + + it "generates correct url based on h/w input" do + cloudflare_url = described_class.cloudflare(image_url, width: 821, height: 420) + expect(cloudflare_url).to match(%r{/width=821,height=420,fit=cover,gravity=auto,format=auto/#{CGI.escape(image_url)}}) + end + + it "does not error if nil" do + cloudflare_url = described_class.cloudflare(nil, width: 821, height: 420) + expect(cloudflare_url).to match(%r{/width=821,height=420,fit=cover,gravity=auto,format=auto/}) + end + + it "pulls suffix if nested cloudflare url is provided" do + cloudflare_url = described_class.cloudflare( + "https://#{ApplicationConfig['CLOUDFLARE_IMAGES_DOMAIN']}/cdn-cgi/image/width=821,height=900,fit=cover,gravity=auto,format=auto/#{CGI.escape(image_url)}", width: 821, height: 420 + ) + expect(cloudflare_url).to eq("https://#{ApplicationConfig['CLOUDFLARE_IMAGES_DOMAIN']}/cdn-cgi/image/width=821,height=420,fit=cover,gravity=auto,format=auto/#{CGI.escape(image_url)}") + end + + it "does not error out if image is empty" do + cloudflare_url = described_class.cloudflare( + "https://#{ApplicationConfig['CLOUDFLARE_IMAGES_DOMAIN']}/cdn-cgi/image/width=821,height=900,fit=cover,gravity=auto,format=auto/", width: 821, height: 420 + ) + expect(cloudflare_url).to eq("https://#{ApplicationConfig['CLOUDFLARE_IMAGES_DOMAIN']}/cdn-cgi/image/width=821,height=420,fit=cover,gravity=auto,format=auto/") + end + + it "does not error out if image is not proper url and has https" do + image_url = "https:hello" + cloudflare_url = described_class.cloudflare( + "https://#{ApplicationConfig['CLOUDFLARE_IMAGES_DOMAIN']}/cdn-cgi/image/width=821,height=900,fit=cover,gravity=auto,format=auto/#{CGI.escape(image_url)}", width: 821, height: 420 + ) + expect(cloudflare_url).to eq("https://#{ApplicationConfig['CLOUDFLARE_IMAGES_DOMAIN']}/cdn-cgi/image/width=821,height=420,fit=cover,gravity=auto,format=auto/https%3Ahello") + end + + it "does not error out if image is not proper url and does not have https" do + image_url = "hello" + cloudflare_url = described_class.cloudflare( + "https://#{ApplicationConfig['CLOUDFLARE_IMAGES_DOMAIN']}/cdn-cgi/image/width=821,height=900,fit=cover,gravity=auto,format=auto/#{CGI.escape(image_url)}", width: 821, height: 420 + ) + expect(cloudflare_url).to eq("https://#{ApplicationConfig['CLOUDFLARE_IMAGES_DOMAIN']}/cdn-cgi/image/width=821,height=420,fit=cover,gravity=auto,format=auto/") + end + end + describe "#cloudinary_enabled?" do it "returns false if cloud_name, api_key or api_secret are missing", :aggregate_failures do allow(Cloudinary.config).to receive(:cloud_name).and_return("") @@ -127,6 +174,18 @@ RSpec.describe Images::Optimizer, type: :service do end end + describe "#cloudflare_enabled?" do + it "returns false if config missing" do + allow(ApplicationConfig).to receive(:[]).with("CLOUDFLARE_IMAGES_DOMAIN").and_return(nil) + expect(described_class.cloudflare_enabled?).to be(false) + end + + it "returns true if config is present" do + allow(ApplicationConfig).to receive(:[]).with("CLOUDFLARE_IMAGES_DOMAIN").and_return("images.com") + expect(described_class.cloudflare_enabled?).to be(true) + end + end + describe "#translate_cloudinary_options" do it "sets resizing_type to fill if crop: fill is provided" do options = { width: 100, height: 100, crop: "fill" }