Class: Dependabot::Composer::Package::PackageDetailsFetcher

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/dependabot/composer/package/package_details_fetcher.rb

Constant Summary collapse

PACKAGE_TYPE =
"composer"
PACKAGE_LANGUAGE =
"php"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dependency:, dependency_files:, credentials:, ignored_versions:, security_advisories:, raise_on_ignored: false) ⇒ PackageDetailsFetcher

Returns a new instance of PackageDetailsFetcher.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 34

def initialize(
  dependency:,
  dependency_files:,
  credentials:,
  ignored_versions:,
  security_advisories:,
  raise_on_ignored: false
)
  @dependency          = dependency
  @dependency_files    = dependency_files
  @credentials         = credentials
  @ignored_versions    = ignored_versions
  @raise_on_ignored    = raise_on_ignored
  @security_advisories = security_advisories

  @registry_urls = T.let(nil, T.nilable(T::Array[String]))
  @registry_version_details = T.let(nil, T.nilable(T::Array[T::Hash[String, T.untyped]]))
end

Instance Attribute Details

#credentialsObject (readonly)

Returns the value of attribute credentials.



60
61
62
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 60

def credentials
  @credentials
end

#dependencyObject (readonly)

Returns the value of attribute dependency.



54
55
56
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 54

def dependency
  @dependency
end

#dependency_filesObject (readonly)

Returns the value of attribute dependency_files.



57
58
59
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 57

def dependency_files
  @dependency_files
end

#ignored_versionsObject (readonly)

Returns the value of attribute ignored_versions.



63
64
65
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 63

def ignored_versions
  @ignored_versions
end

#security_advisoriesObject (readonly)

Returns the value of attribute security_advisories.



66
67
68
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 66

def security_advisories
  @security_advisories
end

Instance Method Details

#auth_jsonObject



264
265
266
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 264

def auth_json
  dependency_files.find { |f| f.name == PackageManager::AUTH_FILENAME }
end

#auth_json_credentialsObject



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 243

def auth_json_credentials
  json = auth_json
  return [] unless json

  parsed_auth_json = JSON.parse(T.must(json.content))
  parsed_auth_json.fetch("http-basic", {}).map do |reg, details|
    Dependabot::Credential.new(
      {
        "registry" => reg,
        "username" => details["username"],
        "password" => details["password"]
      }
    )
  end
rescue JSON::ParserError
  raise Dependabot::DependencyFileNotParseable, json.path if json

  raise Dependabot::DependencyFileNotParseable, "Unknown path"
end

#composer_fileObject



226
227
228
229
230
231
232
233
234
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 226

def composer_file
  composer_file =
    dependency_files.find do |f|
      f.name == PackageManager::MANIFEST_FILENAME
    end
  raise "No #{PackageManager::MANIFEST_FILENAME}!" unless composer_file

  composer_file
end

#extract_versions(listing) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 162

def extract_versions(listing)
  # Packagist's Metadata API format:
  # v1: "packages": {<package name>: {<version_number>: {hash of metadata for a particular release version}}}
  # v2: "packages": {<package name>: [{hash of metadata for a particular release version}]}
  version_listings = listing.dig("packages", dependency.name.downcase)

  if version_listings.is_a?(Hash) # some private registries are still using the v1 format
    # Regardless of API version, composer always reads the version from the metadata hash. So for the v1 API,
    # ignore the keys as repositories other than packagist.org could be using different keys. Instead, coerce
    # to an array of metadata hashes to match v2 format.
    version_listings = version_listings.values
  end

  if version_listings.is_a?(Array)
    version_listings
  else
    []
  end
end

#fetchObject



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 83

def fetch
  available_version_details = registry_version_details
                              .select do |version_details|
    version = version_details.fetch("version")
    version && version_class.correct?(version.gsub(/^v/, ""))
  end

  releases = available_version_details.map do |version_details|
    format_version_release(version_details)
  end
  Dependabot::Package::PackageDetails.new(
    dependency: dependency,
    releases: releases.reverse.uniq(&:version)
  )
end

#fetch_registry_versions_from_url(url) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 128

def fetch_registry_versions_from_url(url)
  url_host = URI(url).host
  cred = registry_credentials.find { |c| url_host == c["registry"] || url_host == URI(T.must(c["registry"])).host } # rubocop:disable Layout/LineLength

  response = Dependabot::RegistryClient.get(
    url: url,
    options: {
      user: cred&.fetch("username", nil),
      password: cred&.fetch("password", nil)
    }
  )

  parse_registry_response(response, url)
rescue Excon::Error::Socket, Excon::Error::Timeout
  []
end

#fetch_releasesObject



69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 69

def fetch_releases
  available_version_details = registry_version_details
                              .select do |version_details|
    version = version_details.fetch("version")
    version && version_class.correct?(version.gsub(/^v/, ""))
  end

  releases = available_version_details.map do |version_details|
    format_version_release(version_details)
  end
  releases
end

#format_version_release(release_data) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 188

def format_version_release(release_data)
  version = release_data["version"].gsub(/^v/, "")
  released_at = release_data["time"] ? Time.parse(release_data["time"]) : nil # this will return nil if the time key is missing, avoiding error # rubocop:disable Layout/LineLength
  url = release_data["dist"] ? release_data["dist"]["url"] : nil
  package_type = PACKAGE_TYPE

  package_release(
    version: version,
    released_at: released_at,
    url: url,
    package_type: package_type
  )
end

#ignore_requirementsObject



269
270
271
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 269

def ignore_requirements
  ignored_versions.map { |req| requirement_class.new(req.split(",")) }
end

#package_release(version:, released_at:, downloads: nil, url: nil, yanked: false, package_type: nil) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 212

def package_release(version:, released_at:, downloads: nil, url: nil, yanked: false, package_type: nil)
  Dependabot::Package::PackageRelease.new(
    version: Composer::Version.new(version),
    released_at: released_at,
    yanked: yanked,
    yanked_reason: nil,
    downloads: downloads,
    url: url,
    package_type: package_type,
    language: nil
  )
end

#parse_registry_response(response, url) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 146

def parse_registry_response(response, url)
  return [] unless response.status == 200

  listing = JSON.parse(response.body)
  return [] if listing.nil?
  return [] unless listing.is_a?(Hash)
  return [] if listing.fetch("packages", []) == []
  return [] unless listing.dig("packages", dependency.name.downcase)

  extract_versions(listing)
rescue JSON::ParserError
  msg = "'#{url}' does not contain valid JSON"
  raise DependencyFileNotResolvable, msg
end

#registry_credentialsObject



237
238
239
240
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 237

def registry_credentials
  credentials.select { |cred| cred["type"] == PackageManager::REPOSITORY_KEY } +
    auth_json_credentials
end

#registry_version_detailsObject



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 102

def registry_version_details
  return @registry_version_details unless @registry_version_details.nil?

  repositories =
    JSON.parse(T.must(composer_file.content))
        .fetch("repositories", []).grep(Hash)

  urls = repositories
         .select { |h| h["type"] == PackageManager::NAME }
         .filter_map { |h| h["url"] }
         .map { |url| url.gsub(%r{\/$}, "") + "/packages.json" }

  unless repositories.any? { |rep| rep["packagist.org"] == false }
    urls << "https://repo.packagist.org/p2/#{dependency.name.downcase}.json"
  end

  @registry_version_details = []
  urls.each do |url|
    @registry_version_details += fetch_registry_versions_from_url(url)
  end

  @registry_version_details.uniq! { |version_details| version_details["version"] }
  @registry_version_details
end

#requirement_classObject



279
280
281
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 279

def requirement_class
  dependency.requirement_class
end

#version_classObject



274
275
276
# File 'lib/dependabot/composer/package/package_details_fetcher.rb', line 274

def version_class
  dependency.version_class
end