dependabot-composer 0.89.0

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.
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/dependency"
4
+ require "dependabot/composer/version"
5
+ require "dependabot/file_parsers"
6
+ require "dependabot/file_parsers/base"
7
+ require "dependabot/shared_helpers"
8
+ require "dependabot/errors"
9
+
10
+ module Dependabot
11
+ module Composer
12
+ class FileParser < Dependabot::FileParsers::Base
13
+ require "dependabot/file_parsers/base/dependency_set"
14
+
15
+ DEPENDENCY_GROUP_KEYS = [
16
+ {
17
+ manifest: "require",
18
+ lockfile: "packages",
19
+ group: "runtime"
20
+ },
21
+ {
22
+ manifest: "require-dev",
23
+ lockfile: "packages-dev",
24
+ group: "development"
25
+ }
26
+ ].freeze
27
+
28
+ def parse
29
+ dependency_set = DependencySet.new
30
+ dependency_set += manifest_dependencies
31
+ dependency_set += lockfile_dependencies
32
+ dependency_set.dependencies
33
+ end
34
+
35
+ private
36
+
37
+ def manifest_dependencies
38
+ dependencies = DependencySet.new
39
+
40
+ DEPENDENCY_GROUP_KEYS.each do |keys|
41
+ next unless parsed_composer_json[keys[:manifest]]
42
+
43
+ parsed_composer_json[keys[:manifest]].each do |name, req|
44
+ next unless package?(name)
45
+
46
+ if lockfile
47
+ version = dependency_version(name: name, type: keys[:group])
48
+
49
+ # Ignore dependencies which appear in the composer.json but not
50
+ # the composer.lock.
51
+ next if version.nil?
52
+
53
+ # Ignore dependency versions which are non-numeric, since they
54
+ # can't be compared later in the process.
55
+ next unless version.match?(/^\d/)
56
+ end
57
+
58
+ dependencies <<
59
+ Dependency.new(
60
+ name: name,
61
+ version: dependency_version(name: name, type: keys[:group]),
62
+ requirements: [{
63
+ requirement: req,
64
+ file: "composer.json",
65
+ source: dependency_source(name: name, type: keys[:group]),
66
+ groups: [keys[:group]]
67
+ }],
68
+ package_manager: "composer"
69
+ )
70
+ end
71
+ end
72
+
73
+ dependencies
74
+ end
75
+
76
+ def lockfile_dependencies
77
+ dependencies = DependencySet.new
78
+
79
+ return dependencies unless lockfile
80
+
81
+ DEPENDENCY_GROUP_KEYS.map { |h| h.fetch(:lockfile) }.each do |key|
82
+ next unless parsed_lockfile[key]
83
+
84
+ parsed_lockfile[key].each do |details|
85
+ name = details["name"]
86
+ next unless package?(name)
87
+
88
+ version = details["version"]&.sub(/^v?/, "")
89
+ next if version.nil?
90
+ next unless version.match?(/^\d/)
91
+
92
+ dependencies <<
93
+ Dependency.new(
94
+ name: name,
95
+ version: version,
96
+ requirements: [],
97
+ package_manager: "composer"
98
+ )
99
+ end
100
+ end
101
+
102
+ dependencies
103
+ end
104
+
105
+ def dependency_version(name:, type:)
106
+ return unless lockfile
107
+
108
+ key = lockfile_key(type)
109
+
110
+ parsed_lockfile.
111
+ fetch(key, []).
112
+ find { |d| d["name"] == name }&.
113
+ fetch("version")&.sub(/^v?/, "")
114
+ end
115
+
116
+ def dependency_source(name:, type:)
117
+ return unless lockfile
118
+
119
+ key = lockfile_key(type)
120
+ package = parsed_lockfile.fetch(key).find { |d| d["name"] == name }
121
+
122
+ return unless package
123
+
124
+ if package["source"].nil? && package.dig("dist", "type") == "path"
125
+ return { type: "path" }
126
+ end
127
+
128
+ return unless package.dig("source", "type") == "git"
129
+
130
+ {
131
+ type: "git",
132
+ url: package.dig("source", "url")
133
+ }
134
+ end
135
+
136
+ def lockfile_key(type)
137
+ case type
138
+ when "runtime" then "packages"
139
+ when "development" then "packages-dev"
140
+ else raise "unknown type #{type}"
141
+ end
142
+ end
143
+
144
+ def package?(name)
145
+ # Filter out php, ext-, composer-plugin-api, and other special
146
+ # packages which don't behave as normal
147
+ name.split("/").count == 2
148
+ end
149
+
150
+ def check_required_files
151
+ raise "No composer.json!" unless get_original_file("composer.json")
152
+ end
153
+
154
+ def parsed_lockfile
155
+ return unless lockfile
156
+
157
+ @parsed_lockfile ||= JSON.parse(lockfile.content)
158
+ rescue JSON::ParserError
159
+ raise Dependabot::DependencyFileNotParseable, lockfile.path
160
+ end
161
+
162
+ def parsed_composer_json
163
+ @parsed_composer_json ||= JSON.parse(composer_json.content)
164
+ rescue JSON::ParserError
165
+ raise Dependabot::DependencyFileNotParseable, composer_json.path
166
+ end
167
+
168
+ def composer_json
169
+ @composer_json ||= get_original_file("composer.json")
170
+ end
171
+
172
+ def lockfile
173
+ @lockfile ||= get_original_file("composer.lock")
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ Dependabot::FileParsers.register("composer", Dependabot::Composer::FileParser)
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/file_updaters"
4
+ require "dependabot/file_updaters/base"
5
+ require "dependabot/shared_helpers"
6
+ require "dependabot/errors"
7
+
8
+ module Dependabot
9
+ module Composer
10
+ class FileUpdater < Dependabot::FileUpdaters::Base
11
+ require_relative "file_updater/manifest_updater"
12
+ require_relative "file_updater/lockfile_updater"
13
+
14
+ def self.updated_files_regex
15
+ [
16
+ /^composer\.json$/,
17
+ /^composer\.lock$/
18
+ ]
19
+ end
20
+
21
+ def updated_dependency_files
22
+ updated_files = []
23
+
24
+ if file_changed?(composer_json)
25
+ updated_files <<
26
+ updated_file(
27
+ file: composer_json,
28
+ content: updated_composer_json_content
29
+ )
30
+ end
31
+
32
+ if lockfile
33
+ updated_files <<
34
+ updated_file(file: lockfile, content: updated_lockfile_content)
35
+ end
36
+
37
+ if updated_files.none? ||
38
+ updated_files.sort_by(&:name) == dependency_files.sort_by(&:name)
39
+ raise "No files have changed!"
40
+ end
41
+
42
+ updated_files
43
+ end
44
+
45
+ private
46
+
47
+ def check_required_files
48
+ raise "No composer.json!" unless get_original_file("composer.json")
49
+ end
50
+
51
+ def updated_composer_json_content
52
+ ManifestUpdater.new(
53
+ dependencies: dependencies,
54
+ manifest: composer_json
55
+ ).updated_manifest_content
56
+ end
57
+
58
+ def updated_lockfile_content
59
+ @updated_lockfile_content ||=
60
+ LockfileUpdater.new(
61
+ dependencies: dependencies,
62
+ dependency_files: dependency_files,
63
+ credentials: credentials
64
+ ).updated_lockfile_content
65
+ end
66
+
67
+ def composer_json
68
+ @composer_json ||= get_original_file("composer.json")
69
+ end
70
+
71
+ def lockfile
72
+ @lockfile ||= get_original_file("composer.lock")
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ Dependabot::FileUpdaters.register("composer", Dependabot::Composer::FileUpdater)
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/shared_helpers"
4
+ require "dependabot/errors"
5
+ require "dependabot/composer/file_updater"
6
+ require "dependabot/composer/version"
7
+ require "dependabot/composer/native_helpers"
8
+
9
+ module Dependabot
10
+ module Composer
11
+ class FileUpdater
12
+ class LockfileUpdater
13
+ require_relative "manifest_updater"
14
+
15
+ def initialize(dependencies:, dependency_files:, credentials:)
16
+ @dependencies = dependencies
17
+ @dependency_files = dependency_files
18
+ @credentials = credentials
19
+ end
20
+
21
+ def updated_lockfile_content
22
+ base_directory = dependency_files.first.directory
23
+ @updated_lockfile_content ||=
24
+ SharedHelpers.in_a_temporary_directory(base_directory) do
25
+ write_temporary_dependency_files
26
+
27
+ updated_content = run_update_helper.fetch("composer.lock")
28
+
29
+ updated_content = post_process_lockfile(updated_content)
30
+ if lockfile.content == updated_content
31
+ raise "Expected content to change!"
32
+ end
33
+
34
+ updated_content
35
+ end
36
+ rescue SharedHelpers::HelperSubprocessFailed => error
37
+ handle_composer_errors(error)
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :dependencies, :dependency_files, :credentials
43
+
44
+ def dependency
45
+ # For now, we'll only ever be updating a single dependency for PHP
46
+ dependencies.first
47
+ end
48
+
49
+ def run_update_helper
50
+ SharedHelpers.with_git_configured(credentials: credentials) do
51
+ SharedHelpers.run_helper_subprocess(
52
+ command: "php #{php_helper_path}",
53
+ function: "update",
54
+ env: credentials_env,
55
+ args: [
56
+ Dir.pwd,
57
+ dependency.name,
58
+ dependency.version,
59
+ git_credentials,
60
+ registry_credentials
61
+ ]
62
+ )
63
+ end
64
+ end
65
+
66
+ def updated_composer_json_content
67
+ ManifestUpdater.new(
68
+ dependencies: dependencies,
69
+ manifest: composer_json
70
+ ).updated_manifest_content
71
+ end
72
+
73
+ # rubocop:disable Metrics/PerceivedComplexity
74
+ # rubocop:disable Metrics/AbcSize
75
+ # rubocop:disable Metrics/CyclomaticComplexity
76
+ # rubocop:disable Metrics/MethodLength
77
+ def handle_composer_errors(error)
78
+ if error.message.start_with?("Failed to execute git checkout")
79
+ raise git_dependency_reference_error(error)
80
+ end
81
+
82
+ if error.message.start_with?("Failed to execute git clone")
83
+ dependency_url =
84
+ error.message.match(/(?:mirror|checkout) '(?<url>.*?)'/).
85
+ named_captures.fetch("url")
86
+ raise GitDependenciesNotReachable, dependency_url
87
+ end
88
+
89
+ if error.message.start_with?("Failed to clone")
90
+ dependency_url =
91
+ error.message.match(/Failed to clone (?<url>.*?) via/).
92
+ named_captures.fetch("url")
93
+ raise GitDependenciesNotReachable, dependency_url
94
+ end
95
+
96
+ if error.message.start_with?("Could not find a key for ACF PRO")
97
+ raise MissingEnvironmentVariable, "ACF_PRO_KEY"
98
+ end
99
+
100
+ if error.message.start_with?("Unknown downloader type: npm-sign") ||
101
+ error.message.include?("file could not be downloaded") ||
102
+ error.message.include?("configuration does not allow connect")
103
+ raise DependencyFileNotResolvable, error.message
104
+ end
105
+
106
+ if error.message.start_with?("Allowed memory size")
107
+ raise Dependabot::OutOfMemory
108
+ end
109
+
110
+ if error.message.include?("403 Forbidden")
111
+ source = error.message.match(%r{https?://(?<source>[^/]+)/}).
112
+ named_captures.fetch("source")
113
+ raise PrivateSourceAuthenticationFailure, source
114
+ end
115
+
116
+ if error.message.include?("Argument 1 passed to Composer")
117
+ msg = "One of your Composer plugins is not compatible with the "\
118
+ "latest version of Composer. Please update Composer and "\
119
+ "try running `composer update` to debug further."
120
+ raise DependencyFileNotResolvable, msg
121
+ end
122
+
123
+ raise error
124
+ end
125
+ # rubocop:enable Metrics/PerceivedComplexity
126
+ # rubocop:enable Metrics/AbcSize
127
+ # rubocop:enable Metrics/CyclomaticComplexity
128
+ # rubocop:enable Metrics/MethodLength
129
+
130
+ def write_temporary_dependency_files
131
+ path_dependencies.each do |file|
132
+ path = file.name
133
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
134
+ File.write(file.name, file.content)
135
+ end
136
+
137
+ File.write("composer.json", locked_composer_json_content)
138
+ File.write("composer.lock", lockfile.content)
139
+ end
140
+
141
+ def locked_composer_json_content
142
+ dependencies.
143
+ reduce(updated_composer_json_content) do |content, dep|
144
+ updated_req = dep.version
145
+ next content unless Composer::Version.correct?(updated_req)
146
+
147
+ old_req =
148
+ dep.requirements.find { |r| r[:file] == "composer.json" }&.
149
+ fetch(:requirement)
150
+
151
+ # When updating a subdep there won't be an old requirement
152
+ next content unless old_req
153
+
154
+ regex =
155
+ /
156
+ "#{Regexp.escape(dep.name)}"\s*:\s*
157
+ "#{Regexp.escape(old_req)}"
158
+ /x
159
+
160
+ content.gsub(regex) do |declaration|
161
+ declaration.gsub(%("#{old_req}"), %("#{updated_req}"))
162
+ end
163
+ end
164
+ end
165
+
166
+ def git_dependency_reference_error(error)
167
+ ref = error.message.match(/checkout '(?<ref>.*?)'/).
168
+ named_captures.fetch("ref")
169
+ dependency_name =
170
+ JSON.parse(lockfile.content).
171
+ values_at("packages", "packages-dev").flatten(1).
172
+ find { |dep| dep.dig("source", "reference") == ref }&.
173
+ fetch("name")
174
+
175
+ raise unless dependency_name
176
+
177
+ raise GitDependencyReferenceNotFound, dependency_name
178
+ end
179
+
180
+ def post_process_lockfile(content)
181
+ content = replace_patches(content)
182
+ replace_content_hash(content)
183
+ end
184
+
185
+ def replace_patches(updated_content)
186
+ content = updated_content
187
+ %w(packages packages-dev).each do |package_type|
188
+ JSON.parse(lockfile.content).fetch(package_type).each do |details|
189
+ next unless details["extra"].is_a?(Hash)
190
+ next unless (patches = details.dig("extra", "patches_applied"))
191
+
192
+ updated_object = JSON.parse(content)
193
+ updated_object_package =
194
+ updated_object.
195
+ fetch(package_type).
196
+ find { |d| d["name"] == details["name"] }
197
+
198
+ next unless updated_object_package
199
+
200
+ updated_object_package["extra"] ||= {}
201
+ updated_object_package["extra"]["patches_applied"] = patches
202
+
203
+ content =
204
+ JSON.pretty_generate(updated_object, indent: " ").
205
+ gsub(/\[\n\n\s*\]/, "[]").
206
+ gsub(/\}\z/, "}\n")
207
+ end
208
+ end
209
+ content
210
+ end
211
+
212
+ def replace_content_hash(content)
213
+ existing_hash = JSON.parse(content).fetch("content-hash")
214
+ SharedHelpers.in_a_temporary_directory do
215
+ File.write("composer.json", updated_composer_json_content)
216
+
217
+ content_hash =
218
+ SharedHelpers.run_helper_subprocess(
219
+ command: "php #{php_helper_path}",
220
+ function: "get_content_hash",
221
+ env: credentials_env,
222
+ args: [Dir.pwd]
223
+ )
224
+
225
+ content.gsub(existing_hash, content_hash)
226
+ end
227
+ end
228
+
229
+ def php_helper_path
230
+ NativeHelpers.composer_helper_path
231
+ end
232
+
233
+ def credentials_env
234
+ credentials.
235
+ select { |c| c.fetch("type") == "php_environment_variable" }.
236
+ map { |cred| [cred["env-key"], cred["env-value"]] }.
237
+ to_h
238
+ end
239
+
240
+ def git_credentials
241
+ credentials.
242
+ select { |cred| cred.fetch("type") == "git_source" }
243
+ end
244
+
245
+ def registry_credentials
246
+ credentials.
247
+ select { |cred| cred.fetch("type") == "composer_repository" }
248
+ end
249
+
250
+ def composer_json
251
+ @composer_json ||=
252
+ dependency_files.find { |f| f.name == "composer.json" }
253
+ end
254
+
255
+ def lockfile
256
+ @lockfile ||=
257
+ dependency_files.find { |f| f.name == "composer.lock" }
258
+ end
259
+
260
+ def path_dependencies
261
+ @path_dependencies ||=
262
+ dependency_files.select { |f| f.name.end_with?("/composer.json") }
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end