dependabot-composer 0.89.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/helpers/.php_cs +32 -0
- data/helpers/bin/run.php +84 -0
- data/helpers/build +14 -0
- data/helpers/composer.json +14 -0
- data/helpers/composer.lock +1528 -0
- data/helpers/php/.php_cs +34 -0
- data/helpers/setup.sh +4 -0
- data/helpers/src/DependabotInstallationManager.php +61 -0
- data/helpers/src/DependabotPluginManager.php +23 -0
- data/helpers/src/ExceptionIO.php +25 -0
- data/helpers/src/Hasher.php +21 -0
- data/helpers/src/UpdateChecker.php +123 -0
- data/helpers/src/Updater.php +97 -0
- data/lib/dependabot/composer.rb +11 -0
- data/lib/dependabot/composer/file_fetcher.rb +132 -0
- data/lib/dependabot/composer/file_parser.rb +179 -0
- data/lib/dependabot/composer/file_updater.rb +78 -0
- data/lib/dependabot/composer/file_updater/lockfile_updater.rb +267 -0
- data/lib/dependabot/composer/file_updater/manifest_updater.rb +66 -0
- data/lib/dependabot/composer/metadata_finder.rb +68 -0
- data/lib/dependabot/composer/native_helpers.rb +20 -0
- data/lib/dependabot/composer/requirement.rb +98 -0
- data/lib/dependabot/composer/update_checker.rb +176 -0
- data/lib/dependabot/composer/update_checker/requirements_updater.rb +253 -0
- data/lib/dependabot/composer/update_checker/version_resolver.rb +214 -0
- data/lib/dependabot/composer/version.rb +26 -0
- metadata +195 -0
@@ -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
|