spree_core 5.3.3 → 5.3.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08d9b1a3cf8bea4f049c9dd444d3f1977e5752adfec8d31dc1fd91d8ee7ccc00'
4
- data.tar.gz: c3653bae9f794377013d94700bee1eaa9da04485737584af25f103f091e662f5
3
+ metadata.gz: 6ed5afbde4c027a14e708b2f10a4585eab43df462da64c93d409336561bd3e45
4
+ data.tar.gz: 7f10df74f02a7770d794793e396486adefcc2349c6e2f0232ed5771b6dec83b2
5
5
  SHA512:
6
- metadata.gz: 987979ba7a79782bd0412b510a3ecf566b61adfc6af241801a547c5b26bd401e3a16475cfdd3712ab29db5c062a5e8dfadea2d7c21634e773b0dd8fbab8f0423
7
- data.tar.gz: 73229e1625bafe9ca1c1d8d00d0d1069bdca226aa91ee462abad53e618e4cd546a09cc242d9829a3f7a74ddae11e0ab69f9e4f9bdaf245ae1b4748af95ecb46a
6
+ metadata.gz: 588d726ccf12398f757ded2e252e1fb90de5a11e2c5863aa06bb27a489dd6ba3e9f1993ef68e6370b4476fa8846f59b85a60046d13953723cec4140b516128ea
7
+ data.tar.gz: 11a5f3e4298506982cecdf1dea096852e500ed157cce75343659252d3573d3e499a3d39ab82553be8e70fb7c8af414e93e016fcb8204b3c6a9cebe03521fcee7
@@ -1,12 +1,15 @@
1
1
  require 'open-uri'
2
2
  require 'openssl'
3
+ require 'ssrf_filter'
4
+ require 'tempfile'
3
5
 
4
6
  module Spree
5
7
  module Images
6
8
  class SaveFromUrlJob < ::Spree::BaseJob
7
9
  queue_as Spree.queues.images
8
- retry_on ActiveRecord::RecordInvalid, OpenURI::HTTPError, wait: :polynomially_longer, attempts: Spree::Config.images_save_from_url_job_attempts.to_i
10
+ retry_on ActiveRecord::RecordInvalid, wait: :polynomially_longer, attempts: Spree::Config.images_save_from_url_job_attempts.to_i
9
11
  discard_on URI::InvalidURIError
12
+ discard_on SsrfFilter::Error
10
13
 
11
14
  def perform(viewable_id, viewable_type, external_url, external_id = nil, position = nil)
12
15
  viewable = viewable_type.safe_constantize.find(viewable_id)
@@ -29,34 +32,55 @@ module Spree
29
32
  # still trigger save! if position has changed
30
33
  image.save! and return if image_already_saved?(image, external_url)
31
34
 
32
- uri = URI.parse(external_url)
33
- unless %w[http https].include?(uri.scheme)
34
- raise URI::InvalidURIError, "Invalid URL scheme: #{uri.scheme}. Only http and https are allowed."
35
- end
36
-
37
- file = uri.open(
38
- 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
39
- 'Accept' => 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
40
- 'Accept-Language' => 'en-US,en;q=0.9',
41
- 'Accept-Encoding' => 'gzip, deflate, br',
42
- 'Cache-Control' => 'no-cache',
43
- 'Pragma' => 'no-cache',
44
- read_timeout: 60,
45
- ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
46
- redirect: true
47
- )
48
- filename = File.basename(uri.path)
49
-
50
- image.attachment.attach(io: file, filename: filename)
51
- image.external_url = external_url
52
- image.external_id = external_id if external_id.present? && image.respond_to?(:external_id)
53
- image.save!
35
+ download_and_attach_image(external_url, image, external_id)
54
36
  rescue ActiveStorage::IntegrityError => e
55
37
  raise e unless Rails.env.test?
56
38
  end
57
39
 
58
40
  private
59
41
 
42
+ def download_and_attach_image(external_url, image, external_id)
43
+ max_size = Spree::Config.max_image_download_size
44
+
45
+ response = SsrfFilter.get(
46
+ external_url,
47
+ headers: {
48
+ 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
49
+ 'Accept' => 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
50
+ 'Accept-Language' => 'en-US,en;q=0.9',
51
+ 'Accept-Encoding' => 'gzip, deflate, br',
52
+ 'Cache-Control' => 'no-cache',
53
+ 'Pragma' => 'no-cache'
54
+ },
55
+ http_options: {
56
+ read_timeout: 60,
57
+ open_timeout: 30
58
+ }
59
+ )
60
+
61
+ body = response.body
62
+ if body.bytesize > max_size
63
+ raise StandardError, "Image file size exceeds the maximum allowed size of #{max_size} bytes"
64
+ end
65
+
66
+ uri = URI.parse(external_url)
67
+ filename = File.basename(uri.path)
68
+ tempfile = Tempfile.new(['spree_image', File.extname(uri.path)], binmode: true)
69
+
70
+ begin
71
+ tempfile.write(body)
72
+ tempfile.rewind
73
+
74
+ image.attachment.attach(io: tempfile, filename: filename)
75
+ image.external_url = external_url
76
+ image.external_id = external_id if external_id.present? && image.respond_to?(:external_id)
77
+ image.save!
78
+ ensure
79
+ tempfile.close
80
+ tempfile.unlink
81
+ end
82
+ end
83
+
60
84
  def image_already_saved?(image, external_url)
61
85
  image.persisted? && image.attachment.attached? && image.external_url.present? && external_url == image.external_url
62
86
  end
@@ -30,6 +30,10 @@ module Spree
30
30
 
31
31
  money_methods :amount
32
32
 
33
+ def amount=(amount)
34
+ self[:amount] = Spree::LocalizedNumber.parse(amount)
35
+ end
36
+
33
37
  self.whitelisted_ransackable_attributes = %w[prefix]
34
38
 
35
39
  def generate_gift_cards
@@ -275,9 +275,9 @@ module Spree
275
275
  # @return [Boolean]
276
276
  def order_refunded?
277
277
  return false if item_count.zero?
278
+ return false if refunds_total.zero?
278
279
 
279
- (payment_state.in?(%w[void failed]) && refunds_total.positive?) ||
280
- refunds_total == total_minus_store_credits - additional_tax_total.abs
280
+ payment_state.in?(%w[void failed]) || refunds_total == total_minus_store_credits - additional_tax_total.abs
281
281
  end
282
282
 
283
283
  def refunds_total
@@ -103,7 +103,7 @@ module Spree
103
103
  translations.with_deleted.each { |rec| rec.update_columns(slug: new_slug.call(rec)) }
104
104
  slugs.with_deleted.each { |rec| rec.update_column(:slug, new_slug.call(rec)) }
105
105
 
106
- translations.find_by!(locale: I18n.locale).update_column(:slug, slug) if Spree.use_translations?
106
+ translations.find_by(locale: I18n.locale)&.update_column(:slug, slug) if Spree.use_translations?
107
107
  end
108
108
  end
109
109
  end
@@ -34,6 +34,10 @@ module Spree
34
34
 
35
35
  delegate :order, :currency, to: :payment
36
36
 
37
+ def amount=(amount)
38
+ self[:amount] = Spree::LocalizedNumber.parse(amount)
39
+ end
40
+
37
41
  def money
38
42
  Spree::Money.new(amount, currency: currency)
39
43
  end
@@ -52,6 +52,10 @@ module Spree
52
52
  extend Spree::DisplayMoney
53
53
  money_methods :amount, :amount_used, :amount_remaining, :amount_authorized
54
54
 
55
+ def amount=(amount)
56
+ self[:amount] = Spree::LocalizedNumber.parse(amount)
57
+ end
58
+
55
59
  self.whitelisted_ransackable_attributes = %w[user_id created_by_id amount currency type_id]
56
60
  self.whitelisted_ransackable_associations = %w[type user created_by]
57
61
 
@@ -91,36 +91,33 @@ module Spree
91
91
  scope :backorderable, -> { left_joins(:stock_items).where(spree_stock_items: { backorderable: true }) }
92
92
  scope :in_stock_or_backorderable, -> { in_stock.or(backorderable) }
93
93
 
94
- scope :eligible, -> {
95
- where(is_master: false).or(
96
- where(
97
- product_id: Spree::Variant.
98
- select(:product_id).
99
- group(:product_id).
100
- having("COUNT(#{Spree::Variant.table_name}.id) = 1")
94
+ scope :eligible, lambda {
95
+ joins(:product).where(
96
+ arel_table[:is_master].eq(false).or(
97
+ Spree::Product.arel_table[:variant_count].eq(0)
101
98
  )
102
99
  )
103
100
  }
104
101
 
105
- scope :not_discontinued, -> do
102
+ scope :not_discontinued, lambda {
106
103
  where(
107
104
  arel_table[:discontinue_on].eq(nil).or(
108
105
  arel_table[:discontinue_on].gteq(Time.current)
109
106
  )
110
107
  )
111
- end
108
+ }
112
109
 
113
110
  scope :not_deleted, -> { where("#{Spree::Variant.quoted_table_name}.deleted_at IS NULL") }
114
111
 
115
- scope :for_currency_and_available_price_amount, ->(currency = nil) do
112
+ scope :for_currency_and_available_price_amount, lambda { |currency = nil|
116
113
  currency ||= Spree::Store.default.default_currency
117
114
  joins(:prices).where("#{Spree::Price.table_name}.currency = ?", currency).where("#{Spree::Price.table_name}.amount IS NOT NULL").distinct
118
- end
115
+ }
119
116
 
120
- scope :active, ->(currency = nil) do
117
+ scope :active, lambda { |currency = nil|
121
118
  not_discontinued.not_deleted.
122
119
  for_currency_and_available_price_amount(currency)
123
- end
120
+ }
124
121
 
125
122
  scope :with_option_value, lambda { |option_name, option_value|
126
123
  option_type_ids = OptionType.where(name: option_name).ids
@@ -187,7 +184,8 @@ module Spree
187
184
  )
188
185
 
189
186
  self.whitelisted_ransackable_associations = %w[option_values product tax_category prices default_price]
190
- self.whitelisted_ransackable_attributes = %w[weight depth width height sku discontinue_on is_master cost_price cost_currency track_inventory deleted_at]
187
+ self.whitelisted_ransackable_attributes = %w[weight depth width height sku discontinue_on is_master cost_price cost_currency track_inventory
188
+ deleted_at]
191
189
  self.whitelisted_ransackable_scopes = %i(product_name_or_sku_cont search_by_product_name_or_sku)
192
190
 
193
191
  def self.product_name_or_sku_cont(query)
@@ -258,9 +256,13 @@ module Spree
258
256
  # @return [String] the options text of the variant
259
257
  def options_text
260
258
  @options_text ||= if option_values.loaded?
261
- option_values.sort_by { |ov| ov.option_type.position }.map { |ov| "#{ov.option_type.presentation}: #{ov.presentation}" }.to_sentence(words_connector: ', ', two_words_connector: ', ')
259
+ option_values.sort_by do |ov|
260
+ ov.option_type.position
261
+ end.map { |ov| "#{ov.option_type.presentation}: #{ov.presentation}" }.to_sentence(words_connector: ', ', two_words_connector: ', ')
262
262
  else
263
- option_values.includes(:option_type).joins(:option_type).order("#{Spree::OptionType.table_name}.position").map { |ov| "#{ov.option_type.presentation}: #{ov.presentation}" }.to_sentence(words_connector: ', ', two_words_connector: ', ')
263
+ option_values.includes(:option_type).joins(:option_type).order("#{Spree::OptionType.table_name}.position").map do |ov|
264
+ "#{ov.option_type.presentation}: #{ov.presentation}"
265
+ end.to_sentence(words_connector: ', ', two_words_connector: ', ')
264
266
  end
265
267
  end
266
268
 
@@ -273,7 +275,7 @@ module Spree
273
275
  # Returns the descriptive name of the variant.
274
276
  # @return [String] the descriptive name of the variant
275
277
  def descriptive_name
276
- is_master? ? name + ' - Master' : name + ' - ' + options_text
278
+ is_master? ? "#{name} - Master" : "#{name} - #{options_text}"
277
279
  end
278
280
 
279
281
  # use deleted? rather than checking the attribute directly. this
@@ -1,17 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ssrf_filter'
4
+ require 'resolv'
5
+
3
6
  module Spree
4
7
  class WebhookEndpoint < Spree.base_class
5
8
  acts_as_paranoid
6
9
 
7
10
  include Spree::SingleStoreResource
8
11
 
12
+ encrypts :secret_key, deterministic: true if Rails.configuration.active_record.encryption.include?(:primary_key)
13
+
9
14
  belongs_to :store, class_name: 'Spree::Store'
10
15
  has_many :webhook_deliveries, class_name: 'Spree::WebhookDelivery', dependent: :destroy_async
11
16
 
12
17
  validates :store, :url, presence: true
13
18
  validates :url, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), message: :invalid_url }
14
19
  validates :active, inclusion: { in: [true, false] }
20
+ validate :url_must_not_resolve_to_private_ip, if: -> { url.present? && url_changed? }
15
21
 
16
22
  before_create :generate_secret_key
17
23
 
@@ -49,5 +55,16 @@ module Spree
49
55
  def generate_secret_key
50
56
  self.secret_key ||= SecureRandom.hex(32)
51
57
  end
58
+
59
+ def url_must_not_resolve_to_private_ip
60
+ uri = URI.parse(url)
61
+ blacklist = SsrfFilter::IPV4_BLACKLIST + SsrfFilter::IPV6_BLACKLIST
62
+ addresses = Resolv.getaddresses(uri.host)
63
+ if addresses.any? { |addr| blacklist.any? { |range| range.include?(IPAddr.new(addr)) } }
64
+ errors.add(:url, :internal_address_not_allowed)
65
+ end
66
+ rescue URI::InvalidURIError, Resolv::ResolvError, IPAddr::InvalidAddressError, ArgumentError
67
+ # URI format validation handles invalid URLs; DNS failures are not SSRF
68
+ end
52
69
  end
53
70
  end
@@ -41,15 +41,10 @@ module Spree
41
41
  end
42
42
 
43
43
  def get_image_link(variant, product)
44
- # try getting image from variant
45
- img = variant.images.first&.plp_url
44
+ image = variant.thumbnail || product.thumbnail
45
+ return if image.nil?
46
46
 
47
- # if no image specified for variant try getting product image
48
- if img.nil?
49
- img = product.images.first&.plp_url
50
- end
51
-
52
- img
47
+ Rails.application.routes.url_helpers.cdn_image_url(image.attachment.variant(:xlarge))
53
48
  end
54
49
 
55
50
  def format_price(variant)
@@ -18,7 +18,9 @@ module Spree
18
18
  result = products_list.call(store)
19
19
  if result.success?
20
20
  result.value[:products].find_each do |product|
21
- product.variants.active.find_each do |variant|
21
+ product.variants_including_master.active.find_each do |variant|
22
+ next if variant.is_master? && product.has_variants?
23
+
22
24
  add_variant_information_to_xml(xml, product, variant)
23
25
  end
24
26
  end
@@ -25,15 +25,16 @@ module Spree
25
25
  return failure(:gift_card_mismatched_customer) if gift_card.user != order.user
26
26
  end
27
27
 
28
- amount = [gift_card.amount_remaining, order.total].min
29
28
  store = order.store
30
29
 
31
- return failure(:gift_card_no_amount_remaining) unless amount.positive? || order.total.zero?
32
-
33
30
  payment_method = ensure_store_credit_payment_method!(store)
34
31
 
35
- gift_card.lock!
36
32
  order.with_lock do
33
+ gift_card.lock!
34
+ amount = [gift_card.amount_remaining, order.total].min
35
+
36
+ return failure(:gift_card_no_amount_remaining) unless amount.positive? || order.total.zero?
37
+
37
38
  store_credit = gift_card.store_credits.create!(
38
39
  store: store,
39
40
  user: order.user,
@@ -45,8 +45,7 @@
45
45
  </div>
46
46
  <%= render "spree/addresses/suggestions_box" %>
47
47
  </div>
48
- <span class="text-sm space-x-2 items-center hidden alert alert-info mt-2 align-items-center" data-address-autocomplete-target="addressWarning">
49
- <%= heroicon "exclamation-circle", variant: :outline if defined?(heroicon) %>
48
+ <span class="text-sm space-x-2 hidden alert alert-info mt-2 static" data-address-autocomplete-target="addressWarning">
50
49
  <%= Spree.t('address_book.add_house_number') %>
51
50
  </span>
52
51
  </div>
@@ -361,6 +361,10 @@ en:
361
361
  cannot_destroy_if_attached_to_line_items: Cannot delete Variants that are added to placed Orders. In such cases, please discontinue them.
362
362
  must_supply_price_for_variant_or_master: Must supply price for variant or master price for product.
363
363
  no_master_variant_found_to_infer_price: No master variant found to infer price
364
+ spree/webhook_endpoint:
365
+ attributes:
366
+ url:
367
+ internal_address_not_allowed: must not point to an internal or private network address
364
368
  spree/wished_item:
365
369
  attributes:
366
370
  variant:
@@ -47,6 +47,7 @@ module Spree
47
47
  preference :expedited_exchanges_days_window, :integer, default: 14 # the amount of days the customer has to return their item after the expedited exchange is shipped in order to avoid being charged
48
48
  preference :geocode_addresses, :boolean, default: true
49
49
  preference :images_save_from_url_job_attempts, :integer, default: 5
50
+ preference :max_image_download_size, :integer, default: 20_971_520 # 20 MB in bytes
50
51
 
51
52
  # Preprocessed product image variant sizes at 2x retina resolution.
52
53
  # These variants are generated on upload to reduce runtime processing.
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- VERSION = '5.3.3'.freeze
2
+ VERSION = '5.3.5'.freeze
3
3
 
4
4
  def self.version
5
5
  VERSION
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.3.3
4
+ version: 5.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Schofield
@@ -573,6 +573,20 @@ dependencies:
573
573
  - - "~>"
574
574
  - !ruby/object:Gem::Version
575
575
  version: '2.0'
576
+ - !ruby/object:Gem::Dependency
577
+ name: ssrf_filter
578
+ requirement: !ruby/object:Gem::Requirement
579
+ requirements:
580
+ - - "~>"
581
+ - !ruby/object:Gem::Version
582
+ version: '1.0'
583
+ type: :runtime
584
+ prerelease: false
585
+ version_requirements: !ruby/object:Gem::Requirement
586
+ requirements:
587
+ - - "~>"
588
+ - !ruby/object:Gem::Version
589
+ version: '1.0'
576
590
  description: Spree Models, Helpers, Services and core libraries
577
591
  email: hello@spreecommerce.org
578
592
  executables: []
@@ -1717,9 +1731,9 @@ licenses:
1717
1731
  - BSD-3-Clause
1718
1732
  metadata:
1719
1733
  bug_tracker_uri: https://github.com/spree/spree/issues
1720
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.3.3
1734
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.3.5
1721
1735
  documentation_uri: https://docs.spreecommerce.org/
1722
- source_code_uri: https://github.com/spree/spree/tree/v5.3.3
1736
+ source_code_uri: https://github.com/spree/spree/tree/v5.3.5
1723
1737
  rdoc_options: []
1724
1738
  require_paths:
1725
1739
  - lib