
livecheck currently doesn't support `POST` requests but it wasn't entirely clear how best to handle that. I initially approached it as a `Post` strategy but unfortunately that would have required us to handle response body parsing (e.g., JSON, XML, etc.) in some fashion. We could borrow some of the logic from related strategies but we would still be stuck having to update `Post` whenever we add a strategy for a new format. Instead, this implements `POST` support by borrowing ideas from the `using: :post` and `data` `url` options found in formulae. This uses a `post_form` option to handle form data and `post_json` to handle JSON data, encoding the hash argument for each into the appropriate format. The presence of either option means that curl will use a `POST` request. With this approach, we can make a `POST` request using any strategy that calls `Strategy::page_headers` or `::page_content` (directly or indirectly) and everything else works the same as usual. The only change needed in related strategies was to pass the options through to the `Strategy` methods. For example, if we need to parse a JSON response from a `POST` request, we add a `post_data` or `post_json` hash to the `livecheck` block `url` and use `strategy :json` with a `strategy` block. This leans on existing patterns that we're already familiar with and shouldn't require any notable maintenance burden when adding new strategies, so it seems like a better approach than a `Post` strategy.
104 lines
3.6 KiB
Ruby
104 lines
3.6 KiB
Ruby
# typed: strict
|
|
# frozen_string_literal: true
|
|
|
|
module Homebrew
|
|
module Livecheck
|
|
module Strategy
|
|
# The {HeaderMatch} strategy follows all URL redirections and scans
|
|
# the resulting headers for matching text using the provided regex.
|
|
#
|
|
# This strategy is not applied automatically and it's necessary to use
|
|
# `strategy :header_match` in a `livecheck` block to apply it.
|
|
class HeaderMatch
|
|
NICE_NAME = "Header match"
|
|
|
|
# A priority of zero causes livecheck to skip the strategy. We do this
|
|
# for {HeaderMatch} so we can selectively apply it when appropriate.
|
|
PRIORITY = 0
|
|
|
|
# The `Regexp` used to determine if the strategy applies to the URL.
|
|
URL_MATCH_REGEX = %r{^https?://}i
|
|
|
|
# The header fields to check when a `strategy` block isn't provided.
|
|
DEFAULT_HEADERS_TO_CHECK = T.let(["content-disposition", "location"].freeze, T::Array[String])
|
|
|
|
# Whether the strategy can be applied to the provided URL.
|
|
#
|
|
# @param url [String] the URL to match against
|
|
# @return [Boolean]
|
|
sig { params(url: String).returns(T::Boolean) }
|
|
def self.match?(url)
|
|
URL_MATCH_REGEX.match?(url)
|
|
end
|
|
|
|
# Identify versions from HTTP headers.
|
|
#
|
|
# @param headers [Hash] a hash of HTTP headers to check for versions
|
|
# @param regex [Regexp, nil] a regex for matching versions
|
|
# @return [Array]
|
|
sig {
|
|
params(
|
|
headers: T::Hash[String, String],
|
|
regex: T.nilable(Regexp),
|
|
block: T.nilable(Proc),
|
|
).returns(T::Array[String])
|
|
}
|
|
def self.versions_from_headers(headers, regex = nil, &block)
|
|
if block
|
|
block_return_value = regex.present? ? yield(headers, regex) : yield(headers)
|
|
return Strategy.handle_block_return(block_return_value)
|
|
end
|
|
|
|
DEFAULT_HEADERS_TO_CHECK.filter_map do |header_name|
|
|
header_value = headers[header_name]
|
|
next if header_value.blank?
|
|
|
|
if regex
|
|
header_value[regex, 1]
|
|
else
|
|
v = Version.parse(header_value, detected_from_url: true)
|
|
v.null? ? nil : v.to_s
|
|
end
|
|
end.uniq
|
|
end
|
|
|
|
# Checks the final URL for new versions after following all redirections,
|
|
# using the provided regex for matching.
|
|
#
|
|
# @param url [String] the URL to fetch
|
|
# @param regex [Regexp, nil] a regex used for matching versions
|
|
# @param homebrew_curl [Boolean] whether to use brewed curl with the URL
|
|
# @return [Hash]
|
|
sig {
|
|
params(
|
|
url: String,
|
|
regex: T.nilable(Regexp),
|
|
homebrew_curl: T::Boolean,
|
|
unused: T.untyped,
|
|
block: T.nilable(Proc),
|
|
).returns(T::Hash[Symbol, T.untyped])
|
|
}
|
|
def self.find_versions(url:, regex: nil, homebrew_curl: false, **unused, &block)
|
|
match_data = { matches: {}, regex:, url: }
|
|
|
|
headers = Strategy.page_headers(
|
|
url,
|
|
url_options: unused.fetch(:url_options, {}),
|
|
homebrew_curl:,
|
|
)
|
|
|
|
# Merge the headers from all responses into one hash
|
|
merged_headers = headers.reduce(&:merge)
|
|
return match_data if merged_headers.blank?
|
|
|
|
versions_from_headers(merged_headers, regex, &block).each do |version_text|
|
|
match_data[:matches][version_text] = Version.new(version_text)
|
|
end
|
|
|
|
match_data
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|